scheletro porting nativo Sailfish OS di Henflow: packaging, settings, pagine, icone, i18n

This commit is contained in:
Carlo Baratto
2026-10-08 22:10:51 +02:00
commit 7cef6954eb
17 changed files with 674 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
# harbour-henflow
Porting **nativo Sailfish OS** dell'app Android *Henflow* (`it.carlobaratto.henflow`,
Gestione ordini uova, galline e consegne). UI in QML/Silica, logica e dati in C++,
pubblicazione prevista sullo **store Jolla Harbour**.
Il porting **non** usa il runtime Flutter community (`flutter-sailfishos`): quello
resta una via OpenRepos, non Harbour-safe, e non supporta i plugin nativi Android
(AdMob/Firebase/Billing). Dati locali sul telefono: nessun account, nessun cloud.
## Stato
Guscio completo e pronto alla prima build (finestra, pagina principale, cover,
impostazioni con lingua, icone, catalogo traduzioni, spec RPM). Le schermate sono
segnaposto: vanno mappate 1:1 dai sorgenti Flutter.
## Struttura
```
harbour-henflow.pro # QT += quick qml, INSTALLS icone
rpm/harbour-henflow.spec # lrelease + desktop cercati con find (layout sorgente variabile)
rpm/harbour-henflow.desktop # profilo [X-Sailjail] esplicito (dati persistenti)
src/main.cpp # SailfishApp, context property appSettings, lingua
src/settings.h/.cpp # QSettings su path esplicito AppConfigLocation + sync()
qml/harbour-henflow.qml # ApplicationWindow: initialPage + cover
qml/pages/MainPage.qml # segnaposto con i moduli previsti
qml/cover/CoverPage.qml
icons/make_icons.py # icone 86/108/128/172 (PIL)
translations/*.ts # stringhe sorgente INGLESI + catalogo italiano
```
## Build e deploy
Sul PC con l'SDK Sailfish (qui non c'e' sfdk):
```sh
sfdk target use SailfishOS-5.0.0.62-aarch64 # o il target del tuo device
sfdk build # RPM in build-*/RPMS/
sfdk deploy # oppure Qt Creator
```
Tarball per lo store (con `--prefix`, esclude .git e i file non tracciati):
```sh
git archive --format=tar.gz --prefix=harbour-henflow-0.1.0/ -o harbour-henflow-0.1.0.tar.gz HEAD
```
## Modello dati proposto (da confermare sui sorgenti Flutter)
Un file JSON in `AppConfigLocation` (`~/.config/harbour/henflow/henflow.json`)
e' sufficiente: nessuna dipendenza extra e nessun driver SQL da verificare sul
device. SQLite resta l'alternativa se servono query piu' ricche.
| entita' | campi |
|---|---|
| `hens` | id, nome, razza, data_nascita, uova_settimana (stima), attiva, note |
| `egg_log` | data, uova_raccolte (+ eventuale id gallina) |
| `customers` | id, nome, telefono, quantita_abituale, giorno_consegna, note |
| `orders` | id, id_cliente, quantita, data_ordine, data_consegna, stato, note |
Derivati: **uova disponibili oggi** = produzione stimata/registrata meno gli ordini
con data di consegna odierna; **data consegna automatica** = primo giorno di
consegna del cliente successivo all'ordine.
## Prossimi passi
1. Copiare i sorgenti Flutter sul server (vedi sotto).
2. Mappare schermate e campi reali (non la descrizione dello store).
3. Implementare storage + schermate, verifica su device (192.168.2.15).
4. Asset store (icona, cover 1080x540), descrizioni EN, upload Harbour.
```sh
# dal PC con i sorgenti Flutter (LAN o VPN)
rsync -av --exclude build --exclude .dart_tool --exclude .git \
~/percorso/henflow/ kaneda@192.168.3.21:/home/kaneda/workspace/henflow-flutter/
```
+45
View File
@@ -0,0 +1,45 @@
TEMPLATE = app
TARGET = harbour-henflow
CONFIG += sailfishapp
CONFIG += c++11
# Il sailfishapp.prf del kit 5.1 NON aggiunge da solo gli include QtQuick/QtQml
# (sintomo: "QQuickView: No such file or directory").
QT += quick qml
SOURCES += \
src/main.cpp \
src/settings.cpp
HEADERS += \
src/settings.h
OTHER_FILES += \
qml/harbour-henflow.qml \
qml/pages/*.qml \
qml/cover/*.qml \
rpm/harbour-henflow.spec \
rpm/harbour-henflow.desktop \
translations/harbour-henflow.ts \
translations/harbour-henflow_it.ts
DISTFILES += $$OTHER_FILES
# Le traduzioni NON passano dal template sailfishapp_i18n (sul kit 5.1 il .qm
# non viene generato/installato in modo affidabile): lo spec compila il catalogo
# con lrelease e lo installa nel datadir dell'app, main.cpp lo carica da li'.
# Le stringhe sorgente sono INGLESI (lingua di default di Sailfish).
# Icone: struttura standard hicolor, path assoluti (gli install manuali con
# path relativi falliscono se il tar sorgente e' incompleto).
icons86.path = /usr/share/icons/hicolor/86x86/apps
icons86.files = $$PWD/icons/86x86/harbour-henflow.png
icons108.path = /usr/share/icons/hicolor/108x108/apps
icons108.files = $$PWD/icons/108x108/harbour-henflow.png
icons128.path = /usr/share/icons/hicolor/128x128/apps
icons128.files = $$PWD/icons/128x128/harbour-henflow.png
icons172.path = /usr/share/icons/hicolor/172x172/apps
icons172.files = $$PWD/icons/172x172/harbour-henflow.png
INSTALLS += icons86 icons108 icons128 icons172
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Genera le icone dell'app in stile Sailfish OS:
sfondo a gradiente ambrato->arancio bruciato con angoli molto arrotondati,
un uovo bianco con ombra morbida e una pagliuzza di grano.
Dimensioni: 86, 108, 128, 172 (struttura standard hicolor)."""
import os
from PIL import Image, ImageDraw
C_TOP = (250, 214, 137) # #FAD689 ambrato chiaro
C_BOT = (196, 122, 41) # #C47A29 arancio bruciato
EGG = (255, 253, 246)
STRAW = (255, 236, 179)
def gradient(size, c1, c2):
img = Image.new("RGB", (size, size))
d = ImageDraw.Draw(img)
for y in range(size):
t = y / (size - 1)
d.line([(0, y), (size, y)],
fill=(int(c1[0] + (c2[0] - c1[0]) * t),
int(c1[1] + (c2[1] - c1[1]) * t),
int(c1[2] + (c2[2] - c1[2]) * t)))
return img
def draw_icon(size):
m = size * 0.05
r = size * 0.22
out = Image.new("RGBA", (size, size), (0, 0, 0, 0))
grad = gradient(size, C_TOP, C_BOT).convert("RGBA")
mask = Image.new("L", (size, size), 0)
ImageDraw.Draw(mask).rounded_rectangle([m, m, size - m, size - m],
radius=r, fill=255)
out.paste(grad, (0, 0), mask)
d = ImageDraw.Draw(out, "RGBA")
# pagliuzza: due archi chiari in basso, suggeriscono il nido
lw = max(2, int(size * 0.03))
for k, off in ((0.0, 0.0), (0.06, 0.03)):
d.arc([size * (0.16 + k), size * (0.62 + off),
size * (0.84 - k), size * (1.02 + off)],
start=200, end=340, fill=STRAW + (110,), width=lw)
# uovo bianco, leggermente spostato in alto, con ombra morbida
ex0, ey0 = size * 0.30, size * 0.22
ex1, ey1 = size * 0.70, size * 0.70
d.ellipse([ex0 + size * 0.015, ey0 + size * 0.02,
ex1 + size * 0.015, ey1 + size * 0.02],
fill=(90, 50, 10, 70))
d.ellipse([ex0, ey0, ex1, ey1], fill=EGG + (255,))
# riflesso
d.ellipse([size * 0.35, size * 0.30, size * 0.45, size * 0.40],
fill=(255, 255, 255, 120))
return out
def main():
for s in (86, 108, 128, 172):
os.makedirs(f"icons/{s}x{s}", exist_ok=True)
draw_icon(s).save(f"icons/{s}x{s}/harbour-henflow.png")
print("icone scritte: 86, 108, 128, 172")
if __name__ == "__main__":
main()
+25
View File
@@ -0,0 +1,25 @@
import QtQuick 2.2
import Sailfish.Silica 1.0
CoverBackground {
Label {
id: title
anchors {
top: parent.top
topMargin: Theme.paddingLarge
horizontalCenter: parent.horizontalCenter
}
text: qsTr("Henflow")
color: Theme.primaryColor
}
Label {
anchors.centerIn: parent
width: parent.width - 2 * Theme.paddingLarge
text: qsTr("Eggs, hens and orders")
font.pixelSize: Theme.fontSizeSmall
color: Theme.secondaryColor
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
}
+12
View File
@@ -0,0 +1,12 @@
import QtQuick 2.2
import Sailfish.Silica 1.0
import "pages"
import "cover"
ApplicationWindow
{
id: app
initialPage: Component { MainPage { } }
cover: Component { CoverPage { } }
}
+68
View File
@@ -0,0 +1,68 @@
import QtQuick 2.2
import Sailfish.Silica 1.0
Page {
id: page
allowedOrientations: Orientation.All
SilicaListView {
id: listView
anchors.fill: parent
header: PageHeader {
title: qsTr("Henflow")
}
model: ListModel {
id: sections
// Moduli previsti dall'app Android, da mappare sulle schermate
// Flutter quando i sorgenti arrivano sul server.
Component.onCompleted: {
append({ "name": qsTr("Hens"), "note": qsTr("name, breed, hatch date, estimated production") })
append({ "name": qsTr("Available eggs"), "note": qsTr("eggs ready today") })
append({ "name": qsTr("Orders"), "note": qsTr("customer, quantity, automatic delivery date") })
append({ "name": qsTr("Calendar"), "note": qsTr("orders and pickups") })
append({ "name": qsTr("Customers"), "note": qsTr("regular buyers") })
append({ "name": qsTr("Dashboard"), "note": qsTr("summary of the day") })
}
}
delegate: ListItem {
id: item
contentHeight: Theme.itemSizeMedium
Column {
anchors.verticalCenter: parent.verticalCenter
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
Label {
text: model.name
color: item.highlighted ? Theme.highlightColor : Theme.primaryColor
truncationMode: TruncationMode.Fade
width: parent.width
}
Label {
text: model.note
font.pixelSize: Theme.fontSizeExtraSmall
color: Theme.secondaryColor
truncationMode: TruncationMode.Fade
width: parent.width
}
}
}
footer: Label {
x: Theme.horizontalPageMargin
width: parent.width - 2 * Theme.horizontalPageMargin
text: qsTr("Native port in progress")
font.pixelSize: Theme.fontSizeExtraSmall
color: Theme.secondaryHighlightColor
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
VerticalScrollDecorator {}
}
}
+20
View File
@@ -0,0 +1,20 @@
[Desktop Entry]
Name=Henflow
Name[it_IT]=Henflow
Comment=Manage hens, eggs and orders
Comment[it_IT]=Gestisci galline, uova e ordini
Exec=harbour-henflow
Icon=harbour-henflow
Type=Application
X-Nemo-Application-Type=silica-qt5
Categories=Utility;
# Profilo SailJail esplicito: senza X-Sailjail la sandbox di Sailfish 5.x usa il
# profilo default, che NASCONDE ~/.config/harbour -> impostazioni e dati non
# persistenti (sintomo: "salva via SSH ma non dal launcher").
# Org/App dichiarati QUI (li legge la sandbox) e nel main.cpp (li legge
# QStandardPaths): AppConfigLocation = ~/.config/harbour/henflow.
# Nessuna permission: l'app e' completamente locale (nessuna rete).
[X-Sailjail]
OrganizationName=harbour
ApplicationName=henflow
+81
View File
@@ -0,0 +1,81 @@
Name: harbour-henflow
Summary: Manage hens, eggs and orders
Version: 0.1.0
Release: 1
Group: Qt/Qt
License: MIT
URL: https://git.hackatoniclife.com/kaneda/harbour-henflow
Source0: %{name}-%{version}.tar.gz
Source1: %{name}.desktop
Requires: sailfishsilica-qt5 >= 0.10.9
BuildRequires: pkgconfig(sailfishapp) >= 1.0.2
BuildRequires: pkgconfig(Qt5Core)
BuildRequires: pkgconfig(Qt5Qml)
BuildRequires: pkgconfig(Qt5Quick)
BuildRequires: qt5-qttools-linguist
BuildRequires: desktop-file-utils
%global debug_package %{nil}
%description
Manage hens, egg production and customer orders locally on the phone.
Chicken registry (name, breed, hatch date), daily egg estimates,
available eggs today, quick order entry with automatic delivery date,
pickup calendar, regular customers and a simple dashboard.
No account, no cloud: all data stays on the device.
%changelog
* Thu Oct 08 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.1.0-1
- Shell del porting nativo Sailfish OS: finestra, pagina principale,
cover, impostazioni (lingua) e pipeline RPM complete
%prep
%setup -q -n %{name}-%{version}
%build
%qmake5
make %{?_smp_mflags}
# Catalogo di traduzione generato esplicitamente con lrelease (il template
# sailfishapp_i18n non produce il .qm in modo affidabile sul kit 5.1).
# Il .ts viene cercato in modo robusto: sfdk usa i sorgenti live (shadow build)
# e la cartella sorgente puo' chiamarsi henflow-0.1.0 (tarball), harbour-henflow
# (clone) o harbour-henflow-main (zip di Gitea).
TS=$(find %{_builddir}/.. -maxdepth 4 -name '%{name}_it.ts' 2>/dev/null | head -1)
if [ -z "$TS" ]; then
echo "ERROR: %{name}_it.ts not found" >&2
exit 1
fi
lrelease "$TS" -qm %{_builddir}/%{name}_it.qm
%install
make install INSTALL_ROOT=%{buildroot}
mkdir -p %{buildroot}%{_datadir}/applications
# Cartella sorgente con nome variabile: cerca il desktop invece di assumere
# un path fisso; la sorgente dichiarata nello spec resta il fallback.
DESKTOP=$(find %{_builddir}/.. -maxdepth 4 -name '%{name}.desktop' 2>/dev/null | head -1)
if [ -z "$DESKTOP" ] && [ -f %{SOURCE1} ]; then DESKTOP=%{SOURCE1}; fi
if [ -z "$DESKTOP" ]; then
echo "ERROR: %{name}.desktop not found" >&2
exit 1
fi
install -m 0644 "$DESKTOP" %{buildroot}%{_datadir}/applications/%{name}.desktop
desktop-file-validate %{buildroot}%{_datadir}/applications/%{name}.desktop || true
# Traduzione italiana nel datadir dell'app (il validatore RPM per Harbour
# rifiuta la cartella di sistema delle traduzioni); main.cpp la carica da qui.
mkdir -p %{buildroot}%{_datadir}/%{name}/translations
install -m 0644 %{_builddir}/%{name}_it.qm %{buildroot}%{_datadir}/%{name}/translations/
%files
%defattr(-,root,root,-)
%{_bindir}/%{name}
%{_datadir}/applications/%{name}.desktop
%{_datadir}/icons/hicolor/86x86/apps/%{name}.png
%{_datadir}/icons/hicolor/108x108/apps/%{name}.png
%{_datadir}/icons/hicolor/128x128/apps/%{name}.png
%{_datadir}/icons/hicolor/172x172/apps/%{name}.png
%{_datadir}/%{name}/qml
%{_datadir}/%{name}/translations/%{name}*.qm
+61
View File
@@ -0,0 +1,61 @@
#include <QCoreApplication>
#include <QDebug>
#include <QGuiApplication>
#include <QLocale>
#include <QQmlContext>
#include <QQuickView>
#include <QTranslator>
#include <sailfishapp.h>
#include "settings.h"
Q_DECL_EXPORT int main(int argc, char *argv[])
{
QGuiApplication *app = SailfishApp::application(argc, argv);
// Identita' allineata al profilo X-Sailjail del desktop file:
// AppConfigLocation = ~/.config/harbour/henflow (zona persistente).
app->setOrganizationName(QStringLiteral("harbour"));
app->setApplicationName(QStringLiteral("henflow"));
app->setApplicationVersion(QStringLiteral("0.1.0"));
Settings settings;
// Lingua scelta dall'utente: "system" segue Sailfish, "en" e' la lingua
// delle stringhe sorgente, "it" carica il catalogo dal datadir dell'app.
QString lang = settings.language();
if (lang == QLatin1String("system")) {
const QString sys = QLocale::system().name();
lang = sys.startsWith(QLatin1String("it")) ? QStringLiteral("it")
: QStringLiteral("en");
}
if (lang == QLatin1String("it")) {
const QList<QTranslator *> trs = app->findChildren<QTranslator *>();
foreach (QTranslator *t, trs) {
app->removeTranslator(t);
t->deleteLater();
}
QTranslator *translator = new QTranslator(app);
if (translator->load(QStringLiteral("harbour-henflow_it"),
QStringLiteral("/usr/share/harbour-henflow/translations"))) {
app->installTranslator(translator);
} else {
qWarning("harbour-henflow: cannot load harbour-henflow_it.qm");
}
}
QQuickView *view = SailfishApp::createView();
qDebug() << "harbour-henflow v0.1.0 build" << __DATE__ << __TIME__;
view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings);
view->rootContext()->setContextProperty(QStringLiteral("appVersion"),
QCoreApplication::applicationVersion());
// pathTo: un QUrl relativo si risolve contro la CWD, che dal launcher non e'
// la cartella dell'app -> QML non caricato e prima schermata BIANCA.
view->setSource(SailfishApp::pathTo(QStringLiteral("qml/harbour-henflow.qml")));
view->show();
return app->exec();
}
+37
View File
@@ -0,0 +1,37 @@
#include "settings.h"
#include <QDir>
#include <QStandardPaths>
Settings::Settings(QObject *parent)
: QObject(parent)
, m_settings(configPath(), QSettings::IniFormat)
{
}
QString Settings::configPath()
{
// AppConfigLocation dipende da organizationName/applicationName impostati
// nel main.cpp: ~/.config/harbour/henflow (zona persistente della sandbox).
const QString dir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
QDir().mkpath(dir);
return dir + QStringLiteral("/henflow.conf");
}
QString Settings::language() const
{
return m_settings.value(QStringLiteral("ui/language"),
QStringLiteral("system")).toString();
}
void Settings::setLanguage(const QString &language)
{
if (language == this->language())
return;
m_settings.setValue(QStringLiteral("ui/language"), language);
// sync() esplicito: su Sailfish l'app puo' chiudersi bruscamente e il flush
// del distruttore non gira -> le scritture non sincronizzate vanno perse.
m_settings.sync();
emit languageChanged();
}
+32
View File
@@ -0,0 +1,32 @@
#ifndef SETTINGS_H
#define SETTINGS_H
#include <QObject>
#include <QSettings>
#include <QString>
// Wrapper QSettings esposto a QML come context property "appSettings".
// NB: si usa un PATH ESPLICITO su AppConfigLocation, non il costruttore
// QSettings(org, app): con la sandbox SailJail il costruttore da solo non
// persiste le modifiche fatte dal launcher.
class Settings : public QObject
{
Q_OBJECT
Q_PROPERTY(QString language READ language WRITE setLanguage NOTIFY languageChanged)
public:
explicit Settings(QObject *parent = nullptr);
QString language() const;
void setLanguage(const QString &language);
signals:
void languageChanged();
private:
static QString configPath();
QSettings m_settings;
};
#endif // SETTINGS_H
+74
View File
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="en" sourcelanguage="en">
<context>
<name>CoverPage</name>
<message>
<source>Henflow</source>
<translation></translation>
</message>
<message>
<source>Eggs, hens and orders</source>
<translation></translation>
</message>
</context>
<context>
<name>MainPage</name>
<message>
<source>Henflow</source>
<translation></translation>
</message>
<message>
<source>Hens</source>
<translation></translation>
</message>
<message>
<source>name, breed, hatch date, estimated production</source>
<translation></translation>
</message>
<message>
<source>Available eggs</source>
<translation></translation>
</message>
<message>
<source>eggs ready today</source>
<translation></translation>
</message>
<message>
<source>Orders</source>
<translation></translation>
</message>
<message>
<source>customer, quantity, automatic delivery date</source>
<translation></translation>
</message>
<message>
<source>Calendar</source>
<translation></translation>
</message>
<message>
<source>orders and pickups</source>
<translation></translation>
</message>
<message>
<source>Customers</source>
<translation></translation>
</message>
<message>
<source>regular buyers</source>
<translation></translation>
</message>
<message>
<source>Dashboard</source>
<translation></translation>
</message>
<message>
<source>summary of the day</source>
<translation></translation>
</message>
<message>
<source>Native port in progress</source>
<translation></translation>
</message>
</context>
</TS>
+74
View File
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="it_IT" sourcelanguage="en">
<context>
<name>CoverPage</name>
<message>
<source>Henflow</source>
<translation>Henflow</translation>
</message>
<message>
<source>Eggs, hens and orders</source>
<translation>Uova, galline e ordini</translation>
</message>
</context>
<context>
<name>MainPage</name>
<message>
<source>Henflow</source>
<translation>Henflow</translation>
</message>
<message>
<source>Hens</source>
<translation>Galline</translation>
</message>
<message>
<source>name, breed, hatch date, estimated production</source>
<translation>nome, razza, data di nascita, produzione stimata</translation>
</message>
<message>
<source>Available eggs</source>
<translation>Uova disponibili</translation>
</message>
<message>
<source>eggs ready today</source>
<translation>uova pronte oggi</translation>
</message>
<message>
<source>Orders</source>
<translation>Ordini</translation>
</message>
<message>
<source>customer, quantity, automatic delivery date</source>
<translation>cliente, quantita&#768;, data di consegna automatica</translation>
</message>
<message>
<source>Calendar</source>
<translation>Calendario</translation>
</message>
<message>
<source>orders and pickups</source>
<translation>ordini e ritiri</translation>
</message>
<message>
<source>Customers</source>
<translation>Clienti</translation>
</message>
<message>
<source>regular buyers</source>
<translation>acquirenti abituali</translation>
</message>
<message>
<source>Dashboard</source>
<translation>Dashboard</translation>
</message>
<message>
<source>summary of the day</source>
<translation>riepilogo della giornata</translation>
</message>
<message>
<source>Native port in progress</source>
<translation>Porting nativo in corso</translation>
</message>
</context>
</TS>