schermate di configurazione complete: razze disponibili, mio pollaio, registro galline, clienti, backup/ripristino nel formato dell'app Android, lingua a runtime, introduzione e informazioni
- datastore: export/import JSON con le chiavi camelCase dell'app Android (breedSettings/reservations/customers), scorte e ultimo aggiornamento preservati al ripristino - settings: flag 'introduzione vista' - desktop: permesso Documents (dove finisce il backup) - nuovi controlli: tools/check_qml.py (chiavi di traduzione, simboli JS, pagine, componenti) e qmllint nella suite - versione 0.2.0
This commit is contained in:
@@ -25,27 +25,44 @@ Android (AdMob/File picker/Share). Nessun account, nessun cloud, come l'original
|
||||
| Home con produzione stimata e coda ordini | fatta |
|
||||
| Scheda ordine (nuovo/modifica, data calcolata) | fatta |
|
||||
| Cover con produzione e ordini pendenti | fatta |
|
||||
| Razze / pollaio / galline, clienti, backup, lingua, intro | da fare |
|
||||
| Razze disponibili, mio pollaio, registro galline | fatte |
|
||||
| Clienti (anagrafica) | fatta |
|
||||
| Backup e ripristino (JSON dell'app Android) | fatto |
|
||||
| Scelta lingua a runtime (4 lingue) | fatta |
|
||||
| Introduzione (primo avvio e dalle impostazioni) | fatta |
|
||||
| Calendario delle consegne | da fare |
|
||||
|
||||
## Struttura
|
||||
|
||||
```
|
||||
harbour-henflow.pro # QT += quick qml, INSTALLS icone
|
||||
harbour-henflow.pro # QT += quick qml, INSTALLS icone + foto razze
|
||||
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/appData, lingua
|
||||
src/settings.h/.cpp # QSettings su path esplicito AppConfigLocation + sync()
|
||||
src/datastore.h/.cpp # dati in un JSON in AppConfigLocation (chiavi = SharedPreferences dell'app)
|
||||
rpm/harbour-henflow.desktop # profilo [X-Sailjail] + permesso Documents (backup)
|
||||
src/main.cpp # SailfishApp, context property appSettings/appData/appVersion
|
||||
src/settings.h/.cpp # QSettings su path esplicito AppConfigLocation + sync() (lingua, intro vista)
|
||||
src/datastore.h/.cpp # dati in un JSON in AppConfigLocation + backup nel formato dell'app Android
|
||||
qml/harbour-henflow.qml # ApplicationWindow: initialPage + cover
|
||||
qml/pages/MainPage.qml # Home: produzione stimata, coda ordini, menu nuovo ordine
|
||||
qml/pages/MainPage.qml # Home: produzione stimata, coda ordini, menu (ordine, clienti, impostazioni)
|
||||
qml/pages/OrderPage.qml # nuovo/modifica ordine con anteprima della data di consegna
|
||||
qml/pages/SettingsPage.qml # indice delle impostazioni
|
||||
qml/pages/BreedsPage.qml # razze disponibili (attiva/disattiva con conferma)
|
||||
qml/pages/CoopPage.qml # mio pollaio: conteggi e produzione per razza
|
||||
qml/pages/BreedDetailPage.qml# scheda razza + registro galline (deposizione, elimina)
|
||||
qml/pages/HenPage.qml # nuova/modifica gallina: nome, data di nascita, deposizione
|
||||
qml/pages/CustomersPage.qml # anagrafica clienti
|
||||
qml/pages/CustomerPage.qml # nuovo/modifica cliente
|
||||
qml/pages/BackupPage.qml # esporta in Documenti / copia negli appunti / ripristina da JSON
|
||||
qml/pages/LanguagePage.qml # lingua (EN/IT/FR/DE) applicata a runtime
|
||||
qml/pages/IntroPage.qml # introduzione in 4 passi
|
||||
qml/pages/AboutPage.qml # informazioni e versione
|
||||
qml/cover/CoverPage.qml # produzione del giorno e ordini pendenti
|
||||
qml/js/henflow.js # motore di calcolo (ES5, testabile con node)
|
||||
qml/js/i18n.js # 126 chiavi x 4 lingue + t(chiave, arg, lingua)
|
||||
qml/js/catalog.js # 7 razze: nome/descrizione/colore uova/foto
|
||||
assets/breeds/*.jpg # foto delle razze (installate nel datadir)
|
||||
tools/gen_from_flutter.py # rigenera i18n.js e catalog.js dai sorgenti Flutter
|
||||
tests/run.sh # motore + traduzioni (node)
|
||||
tools/check_qml.py # controlli statici: chiavi, simboli JS, pagine, componenti
|
||||
tests/run.sh # motore + traduzioni (node) + controlli QML + qmllint
|
||||
tests/cpp_test.sh # livello dati (Qt 5.15 locale, config in cartella temporanea)
|
||||
icons/make_icons.py # icone 86/108/128/172 dall'artwork originale
|
||||
translations/*.ts # stringhe statiche QML (il testo dell'app viene da i18n.js)
|
||||
@@ -54,11 +71,20 @@ translations/*.ts # stringhe statiche QML (il testo dell'app viene da
|
||||
## Test
|
||||
|
||||
```sh
|
||||
sh tests/run.sh # motore di calcolo + traduzioni e catalogo (node, nessuna dipendenza)
|
||||
sh tests/run.sh # motore di calcolo, traduzioni e catalogo (node), controlli
|
||||
# statici della QML (chiavi, simboli JS, pagine, componenti) e qmllint
|
||||
sh tests/cpp_test.sh # livello dati: compila src/datastore.cpp col Qt locale e prova
|
||||
# persistenza, rilettura, compatibilita' delle chiavi, backup, file corrotto
|
||||
```
|
||||
|
||||
I controlli statici della QML servono perche' la UI non e' eseguibile senza SDK:
|
||||
prima di consegnare verificano che ogni chiave di traduzione usata esista in tutte
|
||||
e quattro le lingue, che le funzioni del motore chiamate esistano, che le pagine
|
||||
aperte con `pageStack` esistano davvero e che non si usino componenti inesistenti
|
||||
(es. `Toast`, che in Sailfish non c'e'). `qmllint` aggiunge la sintassi: gli errori
|
||||
di sintassi sono proprio quelli che sul device fanno fallire la pagina con
|
||||
"Impossibile caricare la pagina".
|
||||
|
||||
Il testo dell'app vive in `i18n.js` (come il TranslationService dell'originale), quindi
|
||||
la lingua cambia **a runtime** senza riavviare l'app: cosa che i `.ts` di Qt 5.6 non
|
||||
permettono. La lingua predefinita segue quella del telefono.
|
||||
@@ -89,20 +115,35 @@ Le date sono sempre normalizzate a `YYYY-MM-DD` e le funzioni non mutano l'input
|
||||
|
||||
Nel porting: un unico file JSON in `~/.config/harbour/henflow/` (nessuna
|
||||
dipendenza extra, nessun driver SQL da verificare sul device) con la stessa
|
||||
struttura, cosi' il backup/ripristino resta compatibile con l'app Android.
|
||||
struttura, cosi' i due formati restano leggibili.
|
||||
|
||||
### Backup (formato dell'app Android)
|
||||
|
||||
L'export e il ripristino usano le chiavi **dell'app Android** — `breedSettings`,
|
||||
`reservations`, `customers` (camelCase) — non quelle interne del file: un backup
|
||||
esportato sul telefono Android si ripristina qui e viceversa. Il file viene
|
||||
scritto in `~/Documents` (serve il permesso `Documents` nel profilo SailJail);
|
||||
in alternativa il JSON si copia negli appunti. Scorte e data dell'ultimo
|
||||
aggiornamento non stanno nel backup: al ripristino restano quelli correnti, cosi'
|
||||
non si azzera il magazzino. `tests/cpp_test.sh` prova il round-trip con un backup
|
||||
scritto esattamente come lo esporta l'app Android.
|
||||
|
||||
## Schermate mappate (da sorgenti + screenshot)
|
||||
|
||||
| schermata Flutter | contenuto | porting |
|
||||
|---|---|---|
|
||||
| `OnboardingPage` | 4 passi introduttivi | da fare |
|
||||
| `ModeSelectionPage` | modalita' allevatore / cliente | da decidere |
|
||||
| `HomePage` | tab *Orders list* / *Calendar* (+ magazzino), card produzione stimata, card ordini con stato e azioni (condividi/modifica/elimina), FAB *New order* | da fare |
|
||||
| `SettingsPage` | tab *Available breeds* (catalogo con foto e spunta) / *My coop* (galline registrate, in deposizione, produzione) + backup/ripristino JSON + lingua | da fare |
|
||||
| `BreedDetailPage` | galline della razza, eta' in mesi, stato deposizione | da fare |
|
||||
| `CustomersPage` + dialoghi | anagrafica clienti, nuovo ordine | da fare |
|
||||
| `AboutPage` | info app | da fare |
|
||||
| `CoopIcon` (CustomPainter) | icona pollaio disegnata a mano | da ridisegnare (Canvas QML o PNG) |
|
||||
| `OnboardingPage` | 4 passi introduttivi | `IntroPage` (primo avvio + dalle impostazioni) |
|
||||
| `ModeSelectionPage` | modalita' allevatore / cliente | non portata: solo allevatore, come deciso |
|
||||
| `HomePage` | card produzione stimata, ordini con stato e azioni, FAB *New order* | `MainPage` + `OrderPage` |
|
||||
| `HomePage` tab *Calendar* | calendario delle prenotazioni | da fare |
|
||||
| `SettingsPage` tab *Available breeds* | catalogo con foto e spunta, conferma alla disattivazione | `BreedsPage` |
|
||||
| `SettingsPage` tab *My coop* | razze allevate, galline registrate/in deposizione, produzione | `CoopPage` + `BreedDetailPage` |
|
||||
| `BreedDetailPage` + `AddHenDialog` | galline della razza, eta' in mesi, stato deposizione | `BreedDetailPage` + `HenPage` (data con tre selettori) |
|
||||
| `CustomersPage` + dialoghi | anagrafica clienti | `CustomersPage` + `CustomerPage` |
|
||||
| `AddReservationSheet` | nuovo/modifica ordine | `OrderPage` |
|
||||
| `SettingsPage` backup | esporta su file / importa da JSON | `BackupPage` (formato identico all'Android) |
|
||||
| `AboutPage` | info app | `AboutPage` |
|
||||
| `CoopIcon` (CustomPainter) | icona pollaio disegnata a mano | non portata: la Home usa la card con la produzione |
|
||||
|
||||
Differenze da tenere presenti: **niente annunci** (AdMob non esiste su Sailfish),
|
||||
**share/export** via file JSON in Download invece di `share_plus`/`file_picker`.
|
||||
@@ -120,15 +161,16 @@ 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
|
||||
git archive --format=tar.gz --prefix=harbour-henflow-0.2.0/ -o harbour-henflow-0.2.0.tar.gz HEAD
|
||||
```
|
||||
|
||||
## Aperti
|
||||
|
||||
1. **Schermate da fare**: razze disponibili / mio pollaio / anagrafica galline,
|
||||
clienti, backup-ripristino, scelta lingua, introduzione iniziale, calendario.
|
||||
1. **Calendario delle consegne** (il tab *Calendar* dell'originale): griglia del
|
||||
mese con gli ordini per giorno e l'elenco del giorno scelto.
|
||||
2. **Verifica su device**: la UI Silica non e' verificabile senza SDK/dispositivo
|
||||
(qui si verificano solo motore, traduzioni, catalogo e livello dati).
|
||||
(qui si verificano motore, traduzioni, catalogo, livello dati, chiavi e sintassi
|
||||
della QML, ma non il comportamento a schermo).
|
||||
|
||||
## Correzioni rispetto all'originale (bug trovati nei sorgenti Flutter)
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import QtQuick 2.2
|
||||
import Sailfish.Silica 1.0
|
||||
import "../js/i18n.js" as I18N
|
||||
|
||||
// Informazioni: descrizione dell'app e versione (dall'applicationVersion
|
||||
// impostata nel main.cpp, la stessa del pacchetto RPM).
|
||||
Page {
|
||||
id: page
|
||||
allowedOrientations: Orientation.All
|
||||
|
||||
property string lang: "en"
|
||||
|
||||
function tr(key, arg) { return I18N.t(key, arg, page.lang) }
|
||||
|
||||
Component.onCompleted: page.lang = I18N.langFor(appSettings.language, Qt.locale().name)
|
||||
|
||||
SilicaFlickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: about.height
|
||||
|
||||
Column {
|
||||
id: about
|
||||
width: parent.width
|
||||
spacing: Theme.paddingLarge
|
||||
|
||||
PageHeader { title: page.tr("about") }
|
||||
|
||||
Image {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: Theme.itemSizeLarge * 2
|
||||
height: Theme.itemSizeLarge * 2
|
||||
source: "file:///usr/share/icons/hicolor/172x172/apps/harbour-henflow.png"
|
||||
fillMode: Image.PreserveAspectFit
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "Henflow"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.highlightColor
|
||||
}
|
||||
|
||||
Label {
|
||||
x: Theme.horizontalPageMargin
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
text: page.tr("about_desc")
|
||||
color: Theme.primaryColor
|
||||
wrapMode: Text.Wrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "v " + appVersion
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingLarge }
|
||||
}
|
||||
|
||||
VerticalScrollDecorator {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import QtQuick 2.2
|
||||
import Sailfish.Silica 1.0
|
||||
import "../js/i18n.js" as I18N
|
||||
|
||||
// Backup e ripristino, nello STESSO formato JSON dell'app Android
|
||||
// ({breedSettings, reservations, customers}): i file esportati dalle due
|
||||
// versioni si scambiano senza conversioni.
|
||||
// Qui non esiste la condivisione di Android: l'esportazione scrive il file
|
||||
// nella cartella Documenti (e lo si puo' copiare negli appunti), il
|
||||
// ripristino si fa incollando il JSON.
|
||||
Page {
|
||||
id: page
|
||||
allowedOrientations: Orientation.All
|
||||
|
||||
property string lang: "en"
|
||||
property var onChanged: null
|
||||
property string message: ""
|
||||
property bool messageIsError: false
|
||||
|
||||
function tr(key, arg) { return I18N.t(key, arg, page.lang) }
|
||||
|
||||
function showMessage(text, isError) {
|
||||
page.message = text
|
||||
page.messageIsError = isError
|
||||
hideTimer.restart()
|
||||
}
|
||||
|
||||
function exportData() {
|
||||
var path = appData.exportBackup()
|
||||
if (path.length > 0)
|
||||
page.showMessage(page.tr("export_data") + ": " + path, false)
|
||||
else
|
||||
page.showMessage(page.tr("import_error"), true)
|
||||
}
|
||||
|
||||
function copyData() {
|
||||
clipboard.text = appData.backupJson()
|
||||
page.showMessage(page.tr("copied"), false)
|
||||
}
|
||||
|
||||
function importData() {
|
||||
if (!appData.isValidBackup(backupField.text)) {
|
||||
page.showMessage(page.tr("import_error"), true)
|
||||
return
|
||||
}
|
||||
if (appData.importBackup(backupField.text)) {
|
||||
backupField.text = ""
|
||||
page.showMessage(page.tr("import_success"), false)
|
||||
if (page.onChanged)
|
||||
page.onChanged()
|
||||
} else {
|
||||
page.showMessage(page.tr("import_error"), true)
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: page.lang = I18N.langFor(appSettings.language, Qt.locale().name)
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: 6000
|
||||
onTriggered: page.message = ""
|
||||
}
|
||||
|
||||
Clipboard { id: clipboard }
|
||||
|
||||
SilicaFlickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: content.height
|
||||
|
||||
Column {
|
||||
id: content
|
||||
width: parent.width
|
||||
spacing: Theme.paddingMedium
|
||||
|
||||
PageHeader { title: page.tr("backup_restore") }
|
||||
|
||||
Button {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: page.tr("export_data")
|
||||
onClicked: page.exportData()
|
||||
}
|
||||
|
||||
Button {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: page.tr("copy_to_clipboard")
|
||||
onClicked: page.copyData()
|
||||
}
|
||||
|
||||
Label {
|
||||
x: Theme.horizontalPageMargin
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
text: page.message
|
||||
color: page.messageIsError ? Theme.errorColor : Theme.highlightColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.Wrap
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
SectionHeader { text: page.tr("import_data") }
|
||||
|
||||
TextArea {
|
||||
id: backupField
|
||||
width: parent.width
|
||||
height: Theme.itemSizeLarge * 3
|
||||
placeholderText: page.tr("paste_json_here")
|
||||
label: page.tr("paste_json_here")
|
||||
}
|
||||
|
||||
Button {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: page.tr("import_data")
|
||||
enabled: appData.isValidBackup(backupField.text)
|
||||
onClicked: page.importData()
|
||||
}
|
||||
|
||||
Label {
|
||||
x: Theme.horizontalPageMargin
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
text: appData.dataFilePath()
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeExtraSmall
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingLarge }
|
||||
}
|
||||
|
||||
VerticalScrollDecorator {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
import QtQuick 2.2
|
||||
import Sailfish.Silica 1.0
|
||||
import "../js/henflow.js" as HF
|
||||
import "../js/i18n.js" as I18N
|
||||
import "../js/catalog.js" as Catalog
|
||||
|
||||
// Scheda di una razza allevata: foto, scheda informativa e registro delle
|
||||
// galline (nome, nascita, eta' in mesi, stato) con interruttore deposizione
|
||||
// ed eliminazione con annulla.
|
||||
Page {
|
||||
id: page
|
||||
allowedOrientations: Orientation.All
|
||||
|
||||
property string breedId: ""
|
||||
property string lang: "en"
|
||||
property bool ready: false
|
||||
property var hens: []
|
||||
|
||||
property string breedName: ""
|
||||
property string breedDescription: ""
|
||||
property string eggColor: ""
|
||||
property int registered: 0
|
||||
property int laying: 0
|
||||
property real daily: 0
|
||||
|
||||
function tr(key, arg) { return I18N.t(key, arg, page.lang) }
|
||||
|
||||
ListModel { id: henModel }
|
||||
|
||||
function reload() {
|
||||
page.lang = I18N.langFor(appSettings.language, Qt.locale().name)
|
||||
|
||||
var breed = Catalog.breedFromId(page.breedId)
|
||||
if (breed) {
|
||||
page.breedName = page.tr(breed.name)
|
||||
page.breedDescription = page.tr(breed.description)
|
||||
page.eggColor = page.tr("egg_color") + ": " + page.tr(breed.eggColor)
|
||||
}
|
||||
|
||||
var setting = null
|
||||
var settings = appData.breedSettings()
|
||||
for (var i = 0; i < settings.length; i++) {
|
||||
if (settings[i].breedId === page.breedId)
|
||||
setting = settings[i]
|
||||
}
|
||||
|
||||
page.hens = (setting && setting.hens) ? setting.hens : []
|
||||
page.registered = page.hens.length
|
||||
page.laying = HF.activeHens({ "hens": page.hens })
|
||||
page.daily = page.laying * HF.eggsPerHenPerDay(Catalog.HF_BREEDS, page.breedId)
|
||||
|
||||
var today = HF.todayMidnight()
|
||||
henList.clear()
|
||||
for (var j = 0; j < page.hens.length; j++) {
|
||||
var hen = page.hens[j]
|
||||
var birth = HF.parseIso(hen.birthDate)
|
||||
var months = birth ? HF.ageInMonths(birth, today) : 0
|
||||
var status
|
||||
if (months >= 6)
|
||||
status = hen.isLaying ? page.tr("status_laying") : page.tr("status_rest")
|
||||
else
|
||||
status = hen.isLaying ? page.tr("status_laying") : page.tr("status_young")
|
||||
|
||||
henList.append({
|
||||
"n": j,
|
||||
"name": hen.name,
|
||||
"birth": page.tr("birth") + ": " + HF.formatDay(hen.birthDate),
|
||||
"age": page.tr("age_months", months),
|
||||
"status": status,
|
||||
"isLaying": hen.isLaying
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Salva il registro delle galline di questa razza.
|
||||
function saveHens(list) {
|
||||
var settings = appData.breedSettings()
|
||||
for (var i = 0; i < settings.length; i++) {
|
||||
if (settings[i].breedId === page.breedId)
|
||||
settings[i].hens = list
|
||||
}
|
||||
appData.saveBreedSettings(settings)
|
||||
page.reload()
|
||||
}
|
||||
|
||||
function toggleHen(index) {
|
||||
var list = page.hens.slice()
|
||||
list[index].isLaying = !list[index].isLaying
|
||||
page.saveHens(list)
|
||||
}
|
||||
|
||||
function deleteHen(index) {
|
||||
var list = page.hens.slice()
|
||||
var hen = list[index]
|
||||
remorse.execute(henList, page.tr("delete"), function () {
|
||||
list.splice(index, 1)
|
||||
page.saveHens(list)
|
||||
}, 4000)
|
||||
}
|
||||
|
||||
function openHen(index) {
|
||||
var hen = (index >= 0 && index < page.hens.length) ? page.hens[index] : null
|
||||
pageStack.push(Qt.resolvedUrl("HenPage.qml"),
|
||||
{ "breedId": page.breedId, "hen": hen })
|
||||
}
|
||||
|
||||
Component.onCompleted: page.reload()
|
||||
|
||||
onStatusChanged: {
|
||||
if (status !== PageStatus.Active)
|
||||
return
|
||||
if (page.ready)
|
||||
page.reload()
|
||||
page.ready = true
|
||||
}
|
||||
|
||||
RemorseItem { id: remorse }
|
||||
|
||||
SilicaListView {
|
||||
id: henList
|
||||
anchors.fill: parent
|
||||
|
||||
header: Column {
|
||||
width: henList.width
|
||||
|
||||
PageHeader { title: page.tr("breed_sheet", page.breedName) }
|
||||
|
||||
Image {
|
||||
x: Theme.horizontalPageMargin
|
||||
width: henList.width - 2 * Theme.horizontalPageMargin
|
||||
height: width / 2
|
||||
source: Catalog.breedFromId(page.breedId)
|
||||
? "file://" + Catalog.breedFromId(page.breedId).asset : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingMedium }
|
||||
|
||||
Column {
|
||||
x: Theme.horizontalPageMargin
|
||||
width: henList.width - 2 * Theme.horizontalPageMargin
|
||||
spacing: Theme.paddingSmall
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: page.breedDescription
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: page.eggColor
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeExtraSmall
|
||||
truncationMode: TruncationMode.Fade
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: page.tr("reg_hens") + ": " + page.registered
|
||||
+ " · " + page.tr("laying_6_months") + ": " + page.laying
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeExtraSmall
|
||||
truncationMode: TruncationMode.Fade
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: page.tr("est_prod") + ": " + page.daily.toFixed(1) + " " + page.tr("eggs_per_day")
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeExtraSmall
|
||||
truncationMode: TruncationMode.Fade
|
||||
}
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingMedium }
|
||||
|
||||
SectionHeader { text: page.tr("hen_registry") }
|
||||
}
|
||||
|
||||
model: henModel
|
||||
|
||||
delegate: ListItem {
|
||||
id: henItem
|
||||
contentHeight: henText.height + Theme.paddingMedium
|
||||
|
||||
menu: ContextMenu {
|
||||
MenuItem {
|
||||
text: page.tr("delete")
|
||||
onClicked: page.deleteHen(index)
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: henText
|
||||
x: Theme.horizontalPageMargin
|
||||
y: Theme.paddingMedium
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin - layingSwitch.width
|
||||
- Theme.paddingMedium
|
||||
spacing: Theme.paddingSmall
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: model.name
|
||||
color: henItem.highlighted ? Theme.highlightColor : Theme.primaryColor
|
||||
truncationMode: TruncationMode.Fade
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: model.birth
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeExtraSmall
|
||||
truncationMode: TruncationMode.Fade
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: model.age + " · " + model.status
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeExtraSmall
|
||||
truncationMode: TruncationMode.Fade
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
id: layingSwitch
|
||||
anchors {
|
||||
right: parent.right
|
||||
rightMargin: Theme.horizontalPageMargin
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
checked: model.isLaying
|
||||
onClicked: page.toggleHen(index)
|
||||
}
|
||||
|
||||
onClicked: page.openHen(index)
|
||||
}
|
||||
|
||||
ViewPlaceholder {
|
||||
enabled: henModel.count === 0
|
||||
text: page.tr("no_hens_reg")
|
||||
}
|
||||
|
||||
PullDownMenu {
|
||||
MenuItem {
|
||||
text: page.tr("new_hen")
|
||||
onClicked: page.openHen(-1)
|
||||
}
|
||||
}
|
||||
|
||||
VerticalScrollDecorator {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import QtQuick 2.2
|
||||
import Sailfish.Silica 1.0
|
||||
import "../js/i18n.js" as I18N
|
||||
import "../js/catalog.js" as Catalog
|
||||
|
||||
// "Razze disponibili" (prima scheda del SettingsPage dell'app Flutter):
|
||||
// il catalogo completo con l'interruttore di attivazione. Attivare una razza
|
||||
// crea l'allevamento (senza galline), disattivarla lo elimina dopo conferma.
|
||||
Page {
|
||||
id: page
|
||||
allowedOrientations: Orientation.All
|
||||
|
||||
property string lang: "en"
|
||||
property bool ready: false
|
||||
// razze attive (id) ricostruite dal livello dati
|
||||
property var enabledIds: []
|
||||
|
||||
function tr(key, arg) { return I18N.t(key, arg, page.lang) }
|
||||
|
||||
ListModel { id: breedModel }
|
||||
|
||||
function isEnabled(breedId) {
|
||||
return page.enabledIds.indexOf(breedId) >= 0
|
||||
}
|
||||
|
||||
function henCount(breedId) {
|
||||
var settings = appData.breedSettings()
|
||||
for (var i = 0; i < settings.length; i++) {
|
||||
if (settings[i].breedId === breedId) {
|
||||
var hens = settings[i].hens
|
||||
return hens ? hens.length : 0
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function reload() {
|
||||
page.lang = I18N.langFor(appSettings.language, Qt.locale().name)
|
||||
|
||||
var settings = appData.breedSettings()
|
||||
var ids = []
|
||||
for (var i = 0; i < settings.length; i++)
|
||||
ids.push(settings[i].breedId)
|
||||
page.enabledIds = ids
|
||||
|
||||
breedModel.clear()
|
||||
for (var j = 0; j < Catalog.HF_BREEDS.length; j++) {
|
||||
var b = Catalog.HF_BREEDS[j]
|
||||
breedModel.append({
|
||||
"n": j,
|
||||
"name": page.tr(b.name),
|
||||
"description": page.tr(b.description),
|
||||
"eggColor": page.tr("egg_color") + ": " + page.tr(b.eggColor),
|
||||
"production": b.eggsPerHenPerDay.toFixed(1) + " " + page.tr("eggs_per_g"),
|
||||
"enabled": page.isEnabled(b.id),
|
||||
"hens": page.henCount(b.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Attivazione immediata; disattivazione con conferma (come l'originale,
|
||||
// che avverte perche' si perdono le galline registrate).
|
||||
function toggleBreed(index) {
|
||||
var breed = Catalog.HF_BREEDS[index]
|
||||
if (!breed)
|
||||
return
|
||||
if (page.isEnabled(breed.id)) {
|
||||
confirmDialog.pendingId = breed.id
|
||||
confirmDialog.open()
|
||||
} else {
|
||||
var settings = appData.breedSettings()
|
||||
settings.push({ "breedId": breed.id, "hens": [] })
|
||||
appData.saveBreedSettings(settings)
|
||||
page.reload()
|
||||
}
|
||||
}
|
||||
|
||||
function removeBreed(breedId) {
|
||||
var settings = appData.breedSettings()
|
||||
var out = []
|
||||
for (var i = 0; i < settings.length; i++) {
|
||||
if (settings[i].breedId !== breedId)
|
||||
out.push(settings[i])
|
||||
}
|
||||
appData.saveBreedSettings(out)
|
||||
page.reload()
|
||||
}
|
||||
|
||||
Component.onCompleted: page.reload()
|
||||
|
||||
onStatusChanged: {
|
||||
if (status !== PageStatus.Active)
|
||||
return
|
||||
if (page.ready)
|
||||
page.reload()
|
||||
page.ready = true
|
||||
}
|
||||
|
||||
SilicaListView {
|
||||
id: listView
|
||||
anchors.fill: parent
|
||||
|
||||
header: Column {
|
||||
width: listView.width
|
||||
|
||||
PageHeader { title: page.tr("available_breeds") }
|
||||
}
|
||||
|
||||
model: breedModel
|
||||
|
||||
delegate: ListItem {
|
||||
id: breedItem
|
||||
contentHeight: photo.height + Theme.paddingMedium
|
||||
|
||||
Image {
|
||||
id: photo
|
||||
x: Theme.horizontalPageMargin
|
||||
y: Theme.paddingMedium
|
||||
width: Theme.itemSizeLarge
|
||||
height: Theme.itemSizeLarge
|
||||
source: "file://" + Catalog.HF_BREEDS[model.n].asset
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
Column {
|
||||
x: photo.x + photo.width + Theme.paddingMedium
|
||||
y: Theme.paddingMedium
|
||||
width: parent.width - x - switchControl.width - 2 * Theme.horizontalPageMargin
|
||||
spacing: Theme.paddingSmall
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: model.name
|
||||
color: breedItem.highlighted ? Theme.highlightColor : Theme.primaryColor
|
||||
truncationMode: TruncationMode.Fade
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: model.description
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeExtraSmall
|
||||
wrapMode: Text.Wrap
|
||||
maximumLineCount: 2
|
||||
truncationMode: TruncationMode.Elide
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: model.eggColor
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeExtraSmall
|
||||
truncationMode: TruncationMode.Fade
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: model.hens > 0 ? (model.production + " · " + model.hens + " " + page.tr("hens"))
|
||||
: model.production
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeExtraSmall
|
||||
truncationMode: TruncationMode.Fade
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
id: switchControl
|
||||
anchors {
|
||||
right: parent.right
|
||||
rightMargin: Theme.horizontalPageMargin
|
||||
verticalCenter: photo.verticalCenter
|
||||
}
|
||||
checked: model.enabled
|
||||
onClicked: page.toggleBreed(model.n)
|
||||
}
|
||||
}
|
||||
|
||||
VerticalScrollDecorator {}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: confirmDialog
|
||||
property string pendingId: ""
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
|
||||
DialogHeader {
|
||||
title: page.tr("confirm_deactivation")
|
||||
acceptText: page.tr("deactivate")
|
||||
}
|
||||
|
||||
Label {
|
||||
x: Theme.horizontalPageMargin
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
text: page.tr("delete_msg")
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: page.removeBreed(confirmDialog.pendingId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import QtQuick 2.2
|
||||
import Sailfish.Silica 1.0
|
||||
import "../js/henflow.js" as HF
|
||||
import "../js/i18n.js" as I18N
|
||||
import "../js/catalog.js" as Catalog
|
||||
|
||||
// "Mio pollaio" (seconda scheda dell'originale): produzione del giorno e
|
||||
// razze allevate, con conteggi e stima per razza. Il tocco apre il registro
|
||||
// delle galline di quella razza.
|
||||
Page {
|
||||
id: page
|
||||
allowedOrientations: Orientation.All
|
||||
|
||||
property string lang: "en"
|
||||
property bool ready: false
|
||||
property var settings: []
|
||||
property real totalProduction: 0
|
||||
|
||||
function tr(key, arg) { return I18N.t(key, arg, page.lang) }
|
||||
|
||||
ListModel { id: breedModel }
|
||||
|
||||
function reload() {
|
||||
page.lang = I18N.langFor(appSettings.language, Qt.locale().name)
|
||||
page.settings = appData.breedSettings()
|
||||
page.totalProduction = HF.totalDailyProduction(page.settings, Catalog.HF_BREEDS)
|
||||
|
||||
breedModel.clear()
|
||||
for (var i = 0; i < page.settings.length; i++) {
|
||||
var s = page.settings[i]
|
||||
var breed = Catalog.breedFromId(s.breedId)
|
||||
var laying = HF.activeHens(s)
|
||||
var daily = laying * HF.eggsPerHenPerDay(Catalog.HF_BREEDS, s.breedId)
|
||||
breedModel.append({
|
||||
"n": i,
|
||||
"name": breed ? page.tr(breed.name) : s.breedId,
|
||||
"registered": page.tr("reg_hens") + ": " + (s.hens ? s.hens.length : 0),
|
||||
"laying": page.tr("laying_6_months") + ": " + laying,
|
||||
"production": page.tr("est_prod") + ": " + daily.toFixed(1) + " " + page.tr("eggs_per_day")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: page.reload()
|
||||
|
||||
onStatusChanged: {
|
||||
if (status !== PageStatus.Active)
|
||||
return
|
||||
if (page.ready)
|
||||
page.reload()
|
||||
page.ready = true
|
||||
}
|
||||
|
||||
SilicaListView {
|
||||
id: listView
|
||||
anchors.fill: parent
|
||||
|
||||
header: Column {
|
||||
width: listView.width
|
||||
|
||||
PageHeader { title: page.tr("my_coop") }
|
||||
|
||||
Rectangle {
|
||||
x: Theme.horizontalPageMargin
|
||||
width: listView.width - 2 * Theme.horizontalPageMargin
|
||||
height: coopColumn.height + 2 * Theme.paddingMedium
|
||||
radius: Theme.paddingSmall
|
||||
color: Theme.rgba(Theme.highlightBackgroundColor, 0.12)
|
||||
|
||||
Column {
|
||||
id: coopColumn
|
||||
x: Theme.paddingMedium
|
||||
y: Theme.paddingMedium
|
||||
width: parent.width - 2 * Theme.paddingMedium
|
||||
spacing: Theme.paddingSmall
|
||||
|
||||
Label {
|
||||
text: page.tr("daily_prod_est")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.secondaryColor
|
||||
}
|
||||
|
||||
Label {
|
||||
text: page.totalProduction.toFixed(1) + " " + page.tr("eggs_per_day")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.highlightColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingMedium }
|
||||
|
||||
SectionHeader { text: page.tr("available_breeds") }
|
||||
}
|
||||
|
||||
model: breedModel
|
||||
|
||||
delegate: ListItem {
|
||||
id: coopItem
|
||||
contentHeight: coopText.height + Theme.paddingMedium
|
||||
|
||||
Column {
|
||||
id: coopText
|
||||
x: Theme.horizontalPageMargin
|
||||
y: Theme.paddingMedium
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
spacing: Theme.paddingTiny
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: model.name
|
||||
color: coopItem.highlighted ? Theme.highlightColor : Theme.primaryColor
|
||||
truncationMode: TruncationMode.Fade
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: model.registered + " · " + model.laying
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeExtraSmall
|
||||
truncationMode: TruncationMode.Fade
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: model.production
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeExtraSmall
|
||||
truncationMode: TruncationMode.Fade
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("BreedDetailPage.qml"),
|
||||
{ "breedId": page.settings[index].breedId })
|
||||
}
|
||||
|
||||
ViewPlaceholder {
|
||||
enabled: breedModel.count === 0
|
||||
text: page.tr("no_breeds")
|
||||
hintText: page.tr("config_breed_first")
|
||||
}
|
||||
|
||||
PullDownMenu {
|
||||
MenuItem {
|
||||
text: page.tr("available_breeds")
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("BreedsPage.qml"))
|
||||
}
|
||||
}
|
||||
|
||||
VerticalScrollDecorator {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import QtQuick 2.2
|
||||
import Sailfish.Silica 1.0
|
||||
import "../js/henflow.js" as HF
|
||||
import "../js/i18n.js" as I18N
|
||||
|
||||
// Nuovo cliente / modifica: nome di battesimo (obbligatorio), cognome e
|
||||
// telefono. Stessa regola dell'app Flutter: senza nome non si salva.
|
||||
Page {
|
||||
id: page
|
||||
allowedOrientations: Orientation.All
|
||||
|
||||
property var customer: null // null = nuovo cliente
|
||||
property var onSaved: null
|
||||
property string lang: "en"
|
||||
property string errorMsg: ""
|
||||
|
||||
function tr(key, arg) { return I18N.t(key, arg, page.lang) }
|
||||
|
||||
function reload() {
|
||||
page.lang = I18N.langFor(appSettings.language, Qt.locale().name)
|
||||
if (page.customer) {
|
||||
firstNameField.text = page.customer.firstName || ""
|
||||
lastNameField.text = page.customer.lastName || ""
|
||||
phoneField.text = page.customer.phoneNumber || ""
|
||||
}
|
||||
}
|
||||
|
||||
function save() {
|
||||
var first = firstNameField.text.trim()
|
||||
if (first.length === 0) {
|
||||
page.errorMsg = page.tr("first_name")
|
||||
return
|
||||
}
|
||||
|
||||
var list = appData.customers().slice()
|
||||
var record = {
|
||||
"id": page.customer ? page.customer.id : HF.newId(),
|
||||
"firstName": first,
|
||||
"lastName": lastNameField.text.trim(),
|
||||
"phoneNumber": phoneField.text.trim()
|
||||
}
|
||||
|
||||
if (page.customer) {
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (list[i].id === record.id)
|
||||
list[i] = record
|
||||
}
|
||||
} else {
|
||||
list.push(record)
|
||||
}
|
||||
|
||||
appData.saveCustomers(list)
|
||||
if (page.onSaved)
|
||||
page.onSaved()
|
||||
pageStack.pop()
|
||||
}
|
||||
|
||||
Component.onCompleted: page.reload()
|
||||
|
||||
SilicaFlickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: form.height
|
||||
|
||||
Column {
|
||||
id: form
|
||||
width: parent.width
|
||||
spacing: Theme.paddingMedium
|
||||
|
||||
PageHeader {
|
||||
title: page.customer ? page.tr("edit_customer") : page.tr("new_customer")
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: firstNameField
|
||||
width: parent.width
|
||||
label: page.tr("first_name")
|
||||
EnterKey.enabled: true
|
||||
EnterKey.onClicked: page.save()
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: lastNameField
|
||||
width: parent.width
|
||||
label: page.tr("last_name")
|
||||
EnterKey.enabled: true
|
||||
EnterKey.onClicked: page.save()
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: phoneField
|
||||
width: parent.width
|
||||
label: page.tr("phone_number")
|
||||
inputMethodHints: Qt.ImhDialableCharactersOnly
|
||||
EnterKey.enabled: true
|
||||
EnterKey.onClicked: page.save()
|
||||
}
|
||||
|
||||
Label {
|
||||
x: Theme.horizontalPageMargin
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
text: page.errorMsg
|
||||
color: Theme.errorColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.Wrap
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
Button {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: page.tr("save")
|
||||
onClicked: page.save()
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingLarge }
|
||||
}
|
||||
|
||||
VerticalScrollDecorator {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import QtQuick 2.2
|
||||
import Sailfish.Silica 1.0
|
||||
import "../js/i18n.js" as I18N
|
||||
|
||||
// Clienti abituali: nome, cognome e telefono. Servono a velocizzare
|
||||
// l'inserimento degli ordini (i suggerimenti sono nella scheda ordine).
|
||||
Page {
|
||||
id: page
|
||||
allowedOrientations: Orientation.All
|
||||
|
||||
property string lang: "en"
|
||||
property bool ready: false
|
||||
property var customers: []
|
||||
|
||||
function tr(key, arg) { return I18N.t(key, arg, page.lang) }
|
||||
|
||||
ListModel { id: customerModel }
|
||||
|
||||
function fullName(c) {
|
||||
var n = (c.firstName || "") + " " + (c.lastName || "")
|
||||
return n.trim()
|
||||
}
|
||||
|
||||
function reload() {
|
||||
page.lang = I18N.langFor(appSettings.language, Qt.locale().name)
|
||||
page.customers = appData.customers()
|
||||
|
||||
customerModel.clear()
|
||||
for (var i = 0; i < page.customers.length; i++) {
|
||||
customerModel.append({
|
||||
"n": i,
|
||||
"name": page.fullName(page.customers[i]),
|
||||
"phone": page.customers[i].phoneNumber || ""
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function openCustomer(index) {
|
||||
var customer = (index >= 0 && index < page.customers.length) ? page.customers[index] : null
|
||||
pageStack.push(Qt.resolvedUrl("CustomerPage.qml"),
|
||||
{ "customer": customer, "onSaved": function () { page.reload() } })
|
||||
}
|
||||
|
||||
function deleteCustomer(index) {
|
||||
var list = appData.customers().slice()
|
||||
remorse.execute(listView, page.tr("delete"), function () {
|
||||
list.splice(index, 1)
|
||||
appData.saveCustomers(list)
|
||||
page.reload()
|
||||
}, 4000)
|
||||
}
|
||||
|
||||
Component.onCompleted: page.reload()
|
||||
|
||||
onStatusChanged: {
|
||||
if (status !== PageStatus.Active)
|
||||
return
|
||||
if (page.ready)
|
||||
page.reload()
|
||||
page.ready = true
|
||||
}
|
||||
|
||||
RemorseItem { id: remorse }
|
||||
|
||||
SilicaListView {
|
||||
id: listView
|
||||
anchors.fill: parent
|
||||
|
||||
header: PageHeader { title: page.tr("customers_list") }
|
||||
model: customerModel
|
||||
|
||||
delegate: ListItem {
|
||||
id: customerItem
|
||||
contentHeight: customerText.height + Theme.paddingMedium
|
||||
|
||||
menu: ContextMenu {
|
||||
MenuItem {
|
||||
text: page.tr("edit_customer")
|
||||
onClicked: page.openCustomer(index)
|
||||
}
|
||||
MenuItem {
|
||||
text: page.tr("delete")
|
||||
onClicked: page.deleteCustomer(index)
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: customerText
|
||||
x: Theme.horizontalPageMargin
|
||||
y: Theme.paddingMedium
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
spacing: Theme.paddingSmall
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: model.name
|
||||
color: customerItem.highlighted ? Theme.highlightColor : Theme.primaryColor
|
||||
truncationMode: TruncationMode.Fade
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: model.phone
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeExtraSmall
|
||||
truncationMode: TruncationMode.Fade
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: page.openCustomer(index)
|
||||
}
|
||||
|
||||
ViewPlaceholder {
|
||||
enabled: customerModel.count === 0
|
||||
text: page.tr("no_customers")
|
||||
}
|
||||
|
||||
PullDownMenu {
|
||||
MenuItem {
|
||||
text: page.tr("new_customer")
|
||||
onClicked: page.openCustomer(-1)
|
||||
}
|
||||
}
|
||||
|
||||
VerticalScrollDecorator {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
import QtQuick 2.2
|
||||
import Sailfish.Silica 1.0
|
||||
import "../js/henflow.js" as HF
|
||||
import "../js/i18n.js" as I18N
|
||||
import "../js/catalog.js" as Catalog
|
||||
|
||||
// Nuova gallina / modifica di una gallina: nome, data di nascita e stato di
|
||||
// deposizione. La data si sceglie con tre selettori (giorno, mese, anno):
|
||||
// la combinazione viene mostrata e controllata subito (niente date impossibili).
|
||||
// Come nell'app Flutter, alla scelta della data lo stato si adegua da solo:
|
||||
// sotto i 6 mesi la gallina non e' in deposizione.
|
||||
Page {
|
||||
id: page
|
||||
allowedOrientations: Orientation.All
|
||||
|
||||
property string breedId: ""
|
||||
property var hen: null // null = nuova gallina
|
||||
property string lang: "en"
|
||||
|
||||
property int year: 0
|
||||
property int month: 1
|
||||
property int day: 1
|
||||
property int daysInMonth: 31
|
||||
property bool laying: true
|
||||
property string errorMsg: ""
|
||||
|
||||
readonly property int firstYear: 2010
|
||||
|
||||
function tr(key, arg) { return I18N.t(key, arg, page.lang) }
|
||||
|
||||
function monthLength(y, m) {
|
||||
// giorni del mese: il 31 c'e' solo dove esiste davvero
|
||||
if (m === 2)
|
||||
return (y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0)) ? 29 : 28
|
||||
if (m === 4 || m === 6 || m === 9 || m === 11)
|
||||
return 30
|
||||
return 31
|
||||
}
|
||||
|
||||
function isoOfSelection() {
|
||||
return HF.isoDay(new Date(page.year, page.month - 1, page.day))
|
||||
+ "T00:00:00.000"
|
||||
}
|
||||
|
||||
function monthsOfSelection() {
|
||||
return HF.ageInMonths(new Date(page.year, page.month - 1, page.day), HF.todayMidnight())
|
||||
}
|
||||
|
||||
function refreshSelection() {
|
||||
page.daysInMonth = page.monthLength(page.year, page.month)
|
||||
if (page.day > page.daysInMonth)
|
||||
page.day = page.daysInMonth
|
||||
}
|
||||
|
||||
function applyDefaultDate() {
|
||||
// Come l'originale: si parte da 180 giorni fa (una gallina in deposizione)
|
||||
var d = HF.addDays(HF.todayMidnight(), -180)
|
||||
page.year = d.getFullYear()
|
||||
page.month = d.getMonth() + 1
|
||||
page.day = d.getDate()
|
||||
page.refreshSelection()
|
||||
}
|
||||
|
||||
function reload() {
|
||||
page.lang = I18N.langFor(appSettings.language, Qt.locale().name)
|
||||
|
||||
if (page.hen) {
|
||||
var birth = HF.parseIso(page.hen.birthDate)
|
||||
if (birth) {
|
||||
page.year = birth.getFullYear()
|
||||
page.month = birth.getMonth() + 1
|
||||
page.day = birth.getDate()
|
||||
} else {
|
||||
page.applyDefaultDate()
|
||||
}
|
||||
page.laying = page.hen.isLaying
|
||||
nameField.text = page.hen.name
|
||||
} else {
|
||||
page.applyDefaultDate()
|
||||
page.laying = page.monthsOfSelection() >= 6
|
||||
}
|
||||
page.refreshSelection()
|
||||
}
|
||||
|
||||
function save() {
|
||||
var name = nameField.text.trim()
|
||||
if (name.length === 0) {
|
||||
page.errorMsg = page.tr("hen_name")
|
||||
return
|
||||
}
|
||||
|
||||
var settings = appData.breedSettings()
|
||||
var target = null
|
||||
for (var i = 0; i < settings.length; i++) {
|
||||
if (settings[i].breedId === page.breedId)
|
||||
target = settings[i]
|
||||
}
|
||||
if (!target) {
|
||||
page.errorMsg = page.tr("config_breed_first")
|
||||
return
|
||||
}
|
||||
if (!target.hens)
|
||||
target.hens = []
|
||||
|
||||
var record = {
|
||||
"id": page.hen ? page.hen.id : HF.newId(),
|
||||
"name": name,
|
||||
"birthDate": page.isoOfSelection(),
|
||||
"isLaying": page.laying
|
||||
}
|
||||
|
||||
if (page.hen) {
|
||||
for (var j = 0; j < target.hens.length; j++) {
|
||||
if (target.hens[j].id === record.id)
|
||||
target.hens[j] = record
|
||||
}
|
||||
} else {
|
||||
target.hens.push(record)
|
||||
}
|
||||
|
||||
appData.saveBreedSettings(settings)
|
||||
pageStack.pop()
|
||||
}
|
||||
|
||||
Component.onCompleted: page.reload()
|
||||
|
||||
SilicaFlickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: form.height
|
||||
|
||||
Column {
|
||||
id: form
|
||||
width: parent.width
|
||||
spacing: Theme.paddingMedium
|
||||
|
||||
PageHeader { title: page.tr("new_hen") }
|
||||
|
||||
TextField {
|
||||
id: nameField
|
||||
width: parent.width
|
||||
label: page.tr("hen_name")
|
||||
placeholderText: page.tr("hen_name")
|
||||
EnterKey.enabled: true
|
||||
EnterKey.onClicked: page.save()
|
||||
}
|
||||
|
||||
SectionHeader { text: page.tr("birth_date") }
|
||||
|
||||
Row {
|
||||
x: Theme.horizontalPageMargin
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
spacing: Theme.paddingSmall
|
||||
|
||||
ComboBox {
|
||||
id: dayBox
|
||||
width: (parent.width - 2 * parent.spacing) / 3
|
||||
label: "DD"
|
||||
menu: ContextMenu {
|
||||
Repeater {
|
||||
model: page.daysInMonth
|
||||
delegate: MenuItem {
|
||||
text: String(index + 1)
|
||||
onClicked: {
|
||||
page.day = index + 1
|
||||
dayBox.currentIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: monthBox
|
||||
width: (parent.width - 2 * parent.spacing) / 3
|
||||
label: "MM"
|
||||
menu: ContextMenu {
|
||||
Repeater {
|
||||
model: 12
|
||||
delegate: MenuItem {
|
||||
text: String(index + 1)
|
||||
onClicked: {
|
||||
page.month = index + 1
|
||||
page.refreshSelection()
|
||||
monthBox.currentIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: yearBox
|
||||
width: (parent.width - 2 * parent.spacing) / 3
|
||||
label: "YYYY"
|
||||
menu: ContextMenu {
|
||||
Repeater {
|
||||
model: HF.todayMidnight().getFullYear() - page.firstYear + 1
|
||||
delegate: MenuItem {
|
||||
text: String(page.firstYear + index)
|
||||
onClicked: {
|
||||
page.year = page.firstYear + index
|
||||
page.refreshSelection()
|
||||
yearBox.currentIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
x: Theme.horizontalPageMargin
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
spacing: Theme.paddingSmall
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: page.tr("birth") + ": " + HF.formatDay(page.isoOfSelection())
|
||||
color: Theme.primaryColor
|
||||
truncationMode: TruncationMode.Fade
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: page.tr("age_months", page.monthsOfSelection())
|
||||
+ " · " + (page.monthsOfSelection() >= 6 ? page.tr("laying_6_months") : page.tr("status_young"))
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
truncationMode: TruncationMode.Fade
|
||||
}
|
||||
}
|
||||
|
||||
TextSwitch {
|
||||
id: layingSwitch
|
||||
text: page.tr("in_laying")
|
||||
checked: page.laying
|
||||
onClicked: page.laying = !page.laying
|
||||
}
|
||||
|
||||
Label {
|
||||
x: Theme.horizontalPageMargin
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
text: page.errorMsg
|
||||
color: Theme.errorColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.Wrap
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
Button {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: page.tr("save")
|
||||
onClicked: page.save()
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingLarge }
|
||||
}
|
||||
|
||||
VerticalScrollDecorator {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import QtQuick 2.2
|
||||
import Sailfish.Silica 1.0
|
||||
import "../js/i18n.js" as I18N
|
||||
|
||||
// Introduzione: i quattro passi dell'app Flutter (registro galline, uova,
|
||||
// clienti, ordini). Si apre da sola al primo avvio e resta raggiungibile
|
||||
// dalle impostazioni; in quel caso il pulsante finale torna indietro.
|
||||
Page {
|
||||
id: page
|
||||
allowedOrientations: Orientation.All
|
||||
|
||||
property bool firstRun: true
|
||||
property string lang: "en"
|
||||
|
||||
function tr(key, arg) { return I18N.t(key, arg, page.lang) }
|
||||
|
||||
function finish() {
|
||||
if (page.firstRun)
|
||||
appSettings.introSeen = true
|
||||
pageStack.pop()
|
||||
}
|
||||
|
||||
Component.onCompleted: page.lang = I18N.langFor(appSettings.language, Qt.locale().name)
|
||||
|
||||
SilicaFlickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: intro.height
|
||||
|
||||
Column {
|
||||
id: intro
|
||||
width: parent.width
|
||||
spacing: Theme.paddingMedium
|
||||
|
||||
PageHeader { title: page.tr("welcome") }
|
||||
|
||||
Repeater {
|
||||
model: 4
|
||||
delegate: Column {
|
||||
x: Theme.horizontalPageMargin
|
||||
width: intro.width - 2 * Theme.horizontalPageMargin
|
||||
spacing: Theme.paddingSmall
|
||||
|
||||
SectionHeader {
|
||||
text: index === 0 ? page.tr("intro_step1_title")
|
||||
: index === 1 ? page.tr("intro_step2_title")
|
||||
: index === 2 ? page.tr("intro_step3_title")
|
||||
: page.tr("intro_step4_title")
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: index === 0 ? page.tr("intro_step1_desc")
|
||||
: index === 1 ? page.tr("intro_step2_desc")
|
||||
: index === 2 ? page.tr("intro_step3_desc")
|
||||
: page.tr("intro_step4_desc")
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: page.tr("start_app")
|
||||
onClicked: page.finish()
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingLarge }
|
||||
}
|
||||
|
||||
VerticalScrollDecorator {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import QtQuick 2.2
|
||||
import Sailfish.Silica 1.0
|
||||
import "../js/i18n.js" as I18N
|
||||
|
||||
// Lingua dell'applicazione: le quattro dell'originale (EN/IT/FR/DE).
|
||||
// A differenza di QTranslator, la tabella di traduzione in i18n.js si applica
|
||||
// subito: le pagine che si riaprono usano la lingua nuova senza riavviare.
|
||||
Page {
|
||||
id: page
|
||||
allowedOrientations: Orientation.All
|
||||
|
||||
property string lang: "en"
|
||||
|
||||
function tr(key, arg) { return I18N.t(key, arg, page.lang) }
|
||||
|
||||
ListModel { id: langModel }
|
||||
|
||||
readonly property var codes: [ "en", "it", "fr", "de" ]
|
||||
readonly property var names: [ "English", "Italiano", "Français", "Deutsch" ]
|
||||
|
||||
function reload() {
|
||||
page.lang = I18N.langFor(appSettings.language, Qt.locale().name)
|
||||
langModel.clear()
|
||||
for (var i = 0; i < page.codes.length; i++) {
|
||||
langModel.append({
|
||||
"n": i,
|
||||
"name": page.names[i],
|
||||
"current": page.codes[i] === page.lang
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function choose(index) {
|
||||
var code = page.codes[index]
|
||||
if (appSettings.language === code) {
|
||||
pageStack.pop()
|
||||
return
|
||||
}
|
||||
appSettings.language = code
|
||||
page.reload()
|
||||
}
|
||||
|
||||
Component.onCompleted: page.reload()
|
||||
|
||||
SilicaListView {
|
||||
id: listView
|
||||
anchors.fill: parent
|
||||
|
||||
header: PageHeader { title: page.tr("language") }
|
||||
model: langModel
|
||||
|
||||
delegate: ListItem {
|
||||
id: langItem
|
||||
|
||||
Label {
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: Theme.horizontalPageMargin
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
text: model.name
|
||||
color: langItem.highlighted ? Theme.highlightColor
|
||||
: (model.current ? Theme.highlightColor : Theme.primaryColor)
|
||||
}
|
||||
|
||||
Icon {
|
||||
anchors {
|
||||
right: parent.right
|
||||
rightMargin: Theme.horizontalPageMargin
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
source: "image://theme/icon-m-accept"
|
||||
visible: model.current
|
||||
}
|
||||
|
||||
onClicked: page.choose(index)
|
||||
}
|
||||
|
||||
VerticalScrollDecorator {}
|
||||
}
|
||||
}
|
||||
+33
-2
@@ -36,7 +36,6 @@ Page {
|
||||
page.totalProduction = HF.totalDailyProduction(page.breeds, Catalog.HF_BREEDS)
|
||||
page.productionRows = HF.productionDetail(page.breeds, Catalog.HF_BREEDS)
|
||||
page.rebuildList()
|
||||
page.ready = true
|
||||
}
|
||||
|
||||
// Accrual del magazzino all'apertura (come _loadData in main.dart): i giorni
|
||||
@@ -278,6 +277,14 @@ Page {
|
||||
text: page.tr("new_order")
|
||||
onClicked: page.openOrder(-1)
|
||||
}
|
||||
MenuItem {
|
||||
text: page.tr("customers_list")
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("CustomersPage.qml"))
|
||||
}
|
||||
MenuItem {
|
||||
text: page.tr("settings")
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
|
||||
}
|
||||
}
|
||||
|
||||
footer: Item {
|
||||
@@ -325,5 +332,29 @@ Page {
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: page.reload()
|
||||
Component.onCompleted: {
|
||||
page.reload()
|
||||
|
||||
// Prima apertura: l'introduzione si presenta da sola (come l'originale).
|
||||
// Differita: spingere una pagina mentre la prima si sta creando e'
|
||||
// una corsa che su Qt 5.6 fa fallire l'apertura.
|
||||
if (!appSettings.introSeen)
|
||||
introTimer.start()
|
||||
}
|
||||
|
||||
// Al ritorno dalle altre pagine i dati possono essere cambiati (galline,
|
||||
// ordini, clienti, ripristino di un backup): la Home si ricarica.
|
||||
onStatusChanged: {
|
||||
if (status !== PageStatus.Active)
|
||||
return
|
||||
if (page.ready)
|
||||
page.reload()
|
||||
page.ready = true
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: introTimer
|
||||
interval: 100
|
||||
onTriggered: pageStack.push(Qt.resolvedUrl("IntroPage.qml"), { "firstRun": true })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@ Page {
|
||||
id: previewLabel
|
||||
x: Theme.horizontalPageMargin
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.highlightColor
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import QtQuick 2.2
|
||||
import Sailfish.Silica 1.0
|
||||
import "../js/i18n.js" as I18N
|
||||
|
||||
// Impostazioni: razze disponibili, mio pollaio, clienti, backup, lingua,
|
||||
// introduzione, informazioni. Le voci dell'app Flutter erano tre schede piu'
|
||||
// il selettore di lingua nella barra: qui diventano una lista di voci.
|
||||
Page {
|
||||
id: page
|
||||
allowedOrientations: Orientation.All
|
||||
|
||||
property string lang: "en"
|
||||
// Avvisa la Home che i dati sono cambiati (usato dal ripristino).
|
||||
property var onChanged: null
|
||||
property bool ready: false
|
||||
|
||||
function tr(key, arg) { return I18N.t(key, arg, page.lang) }
|
||||
|
||||
ListModel { id: entries }
|
||||
|
||||
function reload() {
|
||||
page.lang = I18N.langFor(appSettings.language, Qt.locale().name)
|
||||
entries.clear()
|
||||
entries.append({ "label": page.tr("available_breeds") })
|
||||
entries.append({ "label": page.tr("my_coop") })
|
||||
entries.append({ "label": page.tr("customers_list") })
|
||||
entries.append({ "label": page.tr("backup_restore") })
|
||||
entries.append({ "label": page.tr("language") })
|
||||
entries.append({ "label": page.tr("show_intro") })
|
||||
entries.append({ "label": page.tr("about") })
|
||||
}
|
||||
|
||||
function openEntry(index) {
|
||||
if (index === 0) {
|
||||
pageStack.push(Qt.resolvedUrl("BreedsPage.qml"))
|
||||
} else if (index === 1) {
|
||||
pageStack.push(Qt.resolvedUrl("CoopPage.qml"))
|
||||
} else if (index === 2) {
|
||||
pageStack.push(Qt.resolvedUrl("CustomersPage.qml"))
|
||||
} else if (index === 3) {
|
||||
pageStack.push(Qt.resolvedUrl("BackupPage.qml"),
|
||||
{ "onChanged": page.forwardChanged })
|
||||
} else if (index === 4) {
|
||||
pageStack.push(Qt.resolvedUrl("LanguagePage.qml"))
|
||||
} else if (index === 5) {
|
||||
pageStack.push(Qt.resolvedUrl("IntroPage.qml"), { "firstRun": false })
|
||||
} else if (index === 6) {
|
||||
pageStack.push(Qt.resolvedUrl("AboutPage.qml"))
|
||||
}
|
||||
}
|
||||
|
||||
function forwardChanged() {
|
||||
if (page.onChanged)
|
||||
page.onChanged()
|
||||
}
|
||||
|
||||
Component.onCompleted: page.reload()
|
||||
|
||||
// al ritorno da una sottopagina i dati possono essere cambiati
|
||||
onStatusChanged: {
|
||||
if (status !== PageStatus.Active)
|
||||
return
|
||||
if (page.ready)
|
||||
page.reload()
|
||||
page.ready = true
|
||||
}
|
||||
|
||||
SilicaListView {
|
||||
id: listView
|
||||
anchors.fill: parent
|
||||
|
||||
header: PageHeader { title: page.tr("settings") }
|
||||
model: entries
|
||||
|
||||
delegate: ListItem {
|
||||
id: entry
|
||||
|
||||
Label {
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: Theme.horizontalPageMargin
|
||||
right: parent.right
|
||||
rightMargin: Theme.horizontalPageMargin
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
text: model.label
|
||||
color: entry.highlighted ? Theme.highlightColor : Theme.primaryColor
|
||||
truncationMode: TruncationMode.Fade
|
||||
}
|
||||
|
||||
onClicked: page.openEntry(index)
|
||||
}
|
||||
|
||||
VerticalScrollDecorator {}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,9 @@ Categories=Utility;
|
||||
# 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).
|
||||
# Permessi: solo Documenti, dove il backup scrive il file JSON esportato
|
||||
# (nessuna rete: l'app e' completamente locale).
|
||||
[X-Sailjail]
|
||||
OrganizationName=harbour
|
||||
ApplicationName=henflow
|
||||
Permissions=Documents
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Name: harbour-henflow
|
||||
Summary: Manage hens, eggs and orders
|
||||
Version: 0.1.0
|
||||
Version: 0.2.0
|
||||
Release: 1
|
||||
Group: Qt/Qt
|
||||
License: MIT
|
||||
@@ -26,6 +26,11 @@ pickup calendar, regular customers and a simple dashboard.
|
||||
No account, no cloud: all data stays on the device.
|
||||
|
||||
%changelog
|
||||
* Fri Oct 09 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.2.0-1
|
||||
- Porting completo della modalita' allevatore: home con produzione stimata e
|
||||
coda ordini, scheda ordine con data di consegna calcolata, razze disponibili,
|
||||
mio pollaio e registro galline, anagrafica clienti, backup/ripristino nel
|
||||
formato dell'app Android, scelta lingua a runtime, introduzione, informazioni
|
||||
* 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
|
||||
|
||||
+68
-25
@@ -1,5 +1,6 @@
|
||||
#include "datastore.h"
|
||||
|
||||
#include <QDate>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
@@ -19,6 +20,13 @@ const char *kCustomers = "customers_v1";
|
||||
const char *kWarehouseStock = "warehouse_stock_v1";
|
||||
const char *kLastUpdate = "last_production_update_v1";
|
||||
|
||||
// Chiavi del BACKUP: quelle dell'app Android (export/import di SettingsPage),
|
||||
// non quelle del file interno. Non cambiarle: i file devono restare
|
||||
// interscambiabili fra la versione Play Store e questa.
|
||||
const char *kBackupBreeds = "breedSettings";
|
||||
const char *kBackupReservations = "reservations";
|
||||
const char *kBackupCustomers = "customers";
|
||||
|
||||
} // namespace
|
||||
|
||||
DataStore::DataStore(QObject *parent)
|
||||
@@ -160,46 +168,81 @@ bool DataStore::reload()
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool DataStore::exportTo(const QString &path) const
|
||||
// --- backup (formato app Android) -----------------------------------------
|
||||
|
||||
QString DataStore::backupFileName()
|
||||
{
|
||||
return QStringLiteral("henflow-backup-")
|
||||
+ QDate::currentDate().toString(QStringLiteral("yyyy-MM-dd"))
|
||||
+ QStringLiteral(".json");
|
||||
}
|
||||
|
||||
QString DataStore::documentsDir() const
|
||||
{
|
||||
const QString dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
|
||||
if (!dir.isEmpty())
|
||||
QDir().mkpath(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
QString DataStore::backupJson() const
|
||||
{
|
||||
QJsonObject root;
|
||||
root.insert(QLatin1String(kVersion), 1);
|
||||
root.insert(QLatin1String(kBreedSettings), QJsonArray::fromVariantList(m_breedSettings));
|
||||
root.insert(QLatin1String(kReservations), QJsonArray::fromVariantList(m_reservations));
|
||||
root.insert(QLatin1String(kCustomers), QJsonArray::fromVariantList(m_customers));
|
||||
root.insert(QLatin1String(kWarehouseStock), QJsonObject::fromVariantMap(m_stock));
|
||||
if (!m_lastProductionUpdate.isEmpty())
|
||||
root.insert(QLatin1String(kLastUpdate), m_lastProductionUpdate);
|
||||
root.insert(QLatin1String(kBackupBreeds), QJsonArray::fromVariantList(m_breedSettings));
|
||||
root.insert(QLatin1String(kBackupReservations), QJsonArray::fromVariantList(m_reservations));
|
||||
root.insert(QLatin1String(kBackupCustomers), QJsonArray::fromVariantList(m_customers));
|
||||
return QString::fromUtf8(QJsonDocument(root).toJson(QJsonDocument::Indented));
|
||||
}
|
||||
|
||||
QString DataStore::exportBackup() const
|
||||
{
|
||||
const QString dir = documentsDir();
|
||||
if (dir.isEmpty()) {
|
||||
qWarning() << "henflow: no Documents location for the backup";
|
||||
return QString();
|
||||
}
|
||||
const QString path = dir + QLatin1Char('/') + backupFileName();
|
||||
|
||||
QSaveFile out(path);
|
||||
if (!out.open(QIODevice::WriteOnly)) {
|
||||
qWarning() << "henflow: cannot write backup" << path << out.errorString();
|
||||
return false;
|
||||
return QString();
|
||||
}
|
||||
out.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
|
||||
return out.commit();
|
||||
out.write(backupJson().toUtf8());
|
||||
if (!out.commit()) {
|
||||
qWarning() << "henflow: backup commit failed" << out.errorString();
|
||||
return QString();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
bool DataStore::importFrom(const QString &path)
|
||||
bool DataStore::isValidBackup(const QString &jsonText) const
|
||||
{
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qWarning() << "henflow: cannot read backup" << path << file.errorString();
|
||||
if (jsonText.trimmed().isEmpty())
|
||||
return false;
|
||||
}
|
||||
QJsonParseError error;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
|
||||
if (error.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
qWarning() << "henflow: backup non valido" << path << error.errorString();
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(jsonText.toUtf8(), &error);
|
||||
if (error.error != QJsonParseError::NoError || !doc.isObject())
|
||||
return false;
|
||||
const QJsonObject root = doc.object();
|
||||
// Le due liste indispensabili; i clienti possono mancare (backup vecchi).
|
||||
return root.value(QLatin1String(kBackupBreeds)).isArray()
|
||||
&& root.value(QLatin1String(kBackupReservations)).isArray();
|
||||
}
|
||||
|
||||
bool DataStore::importBackup(const QString &jsonText)
|
||||
{
|
||||
if (!isValidBackup(jsonText)) {
|
||||
qWarning() << "henflow: backup non valido (attese le chiavi breedSettings/reservations)";
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonObject root = doc.object();
|
||||
m_breedSettings = root.value(QLatin1String(kBreedSettings)).toArray().toVariantList();
|
||||
m_reservations = root.value(QLatin1String(kReservations)).toArray().toVariantList();
|
||||
m_customers = root.value(QLatin1String(kCustomers)).toArray().toVariantList();
|
||||
m_stock = root.value(QLatin1String(kWarehouseStock)).toObject().toVariantMap();
|
||||
m_lastProductionUpdate = root.value(QLatin1String(kLastUpdate)).toString();
|
||||
const QJsonObject root = QJsonDocument::fromJson(jsonText.toUtf8()).object();
|
||||
m_breedSettings = root.value(QLatin1String(kBackupBreeds)).toArray().toVariantList();
|
||||
m_reservations = root.value(QLatin1String(kBackupReservations)).toArray().toVariantList();
|
||||
m_customers = root.value(QLatin1String(kBackupCustomers)).toArray().toVariantList();
|
||||
// Scorte e ultimo aggiornamento NON stanno nel backup Android: si tengono
|
||||
// quelli correnti, cosi' il ripristino non azzera il magazzino.
|
||||
setDirty();
|
||||
return store();
|
||||
}
|
||||
|
||||
+16
-7
@@ -7,12 +7,15 @@
|
||||
#include <QVariantMap>
|
||||
|
||||
// Persistenza dei dati di Henflow: un unico file JSON in AppConfigLocation
|
||||
// (~/.config/harbour/henflow/henflow.json), con la STESSA struttura delle
|
||||
// chiavi SharedPreferences dell'app Flutter, cosi' il backup resta
|
||||
// compatibile con la versione Android.
|
||||
// (~/.config/harbour/henflow/henflow.json). Le chiavi sono quelle
|
||||
// SharedPreferences dell'app Flutter, cosi' i due formati restano leggibili.
|
||||
//
|
||||
// Il calcolo (produzione, coda FIFO, date) NON sta qui: e' in qml/js/henflow.js,
|
||||
// dove e' verificabile con node. Questa classe legge e scrive soltanto.
|
||||
//
|
||||
// Il BACKUP invece e' nel formato esatto dell'app Android
|
||||
// ({breedSettings, reservations, customers}), perche' deve essere
|
||||
// interscambiabile con i file esportati dalla versione Play Store.
|
||||
class DataStore : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -35,16 +38,21 @@ public:
|
||||
// Rilegge dal disco (usato quando l'app torna in primo piano).
|
||||
Q_INVOKABLE bool reload();
|
||||
|
||||
// Percorso del file dati, per diagnostica e per il backup.
|
||||
// Percorso del file dati, per diagnostica.
|
||||
Q_INVOKABLE QString dataFilePath() const;
|
||||
|
||||
// Backup/ripristino: copia dell'intero file JSON.
|
||||
Q_INVOKABLE bool exportTo(const QString &path) const;
|
||||
Q_INVOKABLE bool importFrom(const QString &path);
|
||||
// Backup/ripristino nel formato dell'app Android.
|
||||
Q_INVOKABLE QString documentsDir() const;
|
||||
Q_INVOKABLE QString backupJson() const; // contenuto pronto da copiare
|
||||
Q_INVOKABLE QString exportBackup() const; // scrive in Documenti -> percorso (vuoto = errore)
|
||||
Q_INVOKABLE bool importBackup(const QString &jsonText); // sostituisce i dati; false se il formato non va
|
||||
|
||||
// Cancella tutto (voce "azzera dati" nelle impostazioni).
|
||||
Q_INVOKABLE bool clearAll();
|
||||
|
||||
// Una voce di backup e' valida? (usato dall'interfaccia per abilitare Importa)
|
||||
Q_INVOKABLE bool isValidBackup(const QString &jsonText) const;
|
||||
|
||||
signals:
|
||||
void changed();
|
||||
|
||||
@@ -54,6 +62,7 @@ private:
|
||||
bool store();
|
||||
|
||||
static QString dataPath();
|
||||
static QString backupFileName();
|
||||
|
||||
QVariantList m_breedSettings;
|
||||
QVariantList m_reservations;
|
||||
|
||||
+2
-2
@@ -18,7 +18,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||
// AppConfigLocation = ~/.config/harbour/henflow (zona persistente).
|
||||
app->setOrganizationName(QStringLiteral("harbour"));
|
||||
app->setApplicationName(QStringLiteral("henflow"));
|
||||
app->setApplicationVersion(QStringLiteral("0.1.0"));
|
||||
app->setApplicationVersion(QStringLiteral("0.2.0"));
|
||||
|
||||
Settings settings;
|
||||
DataStore data;
|
||||
@@ -48,7 +48,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||
|
||||
QQuickView *view = SailfishApp::createView();
|
||||
|
||||
qDebug() << "harbour-henflow v0.1.0 build" << __DATE__ << __TIME__;
|
||||
qDebug() << "harbour-henflow v0.2.0 build" << __DATE__ << __TIME__;
|
||||
|
||||
view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings);
|
||||
view->rootContext()->setContextProperty(QStringLiteral("appData"), &data);
|
||||
|
||||
@@ -35,3 +35,18 @@ void Settings::setLanguage(const QString &language)
|
||||
m_settings.sync();
|
||||
emit languageChanged();
|
||||
}
|
||||
|
||||
bool Settings::introSeen() const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("ui/intro_seen"), false).toBool();
|
||||
}
|
||||
|
||||
void Settings::setIntroSeen(bool seen)
|
||||
{
|
||||
if (seen == introSeen())
|
||||
return;
|
||||
|
||||
m_settings.setValue(QStringLiteral("ui/intro_seen"), seen);
|
||||
m_settings.sync();
|
||||
emit introSeenChanged();
|
||||
}
|
||||
|
||||
@@ -13,15 +13,22 @@ class Settings : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString language READ language WRITE setLanguage NOTIFY languageChanged)
|
||||
Q_PROPERTY(bool introSeen READ introSeen WRITE setIntroSeen NOTIFY introSeenChanged)
|
||||
|
||||
public:
|
||||
explicit Settings(QObject *parent = nullptr);
|
||||
|
||||
// "system", "it", "en", "fr", "de" (come l'app Android).
|
||||
QString language() const;
|
||||
void setLanguage(const QString &language);
|
||||
|
||||
// L'introduzione e' stata mostrata? (primo avvio)
|
||||
bool introSeen() const;
|
||||
void setIntroSeen(bool seen);
|
||||
|
||||
signals:
|
||||
void languageChanged();
|
||||
void introSeenChanged();
|
||||
|
||||
private:
|
||||
static QString configPath();
|
||||
|
||||
+5
-1
@@ -13,9 +13,13 @@ export PATH LD_LIBRARY_PATH
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
# HOME temporanea: anche DocumentsLocation (dove finisce il backup) resta nel
|
||||
# temporaneo, cosi' il banco di prova non scrive nella cartella vera dell'utente.
|
||||
HOME="$WORK/home"
|
||||
XDG_CONFIG_HOME="$WORK/config"
|
||||
XDG_DATA_HOME="$WORK/data"
|
||||
export XDG_CONFIG_HOME XDG_DATA_HOME
|
||||
mkdir -p "$HOME" "$XDG_CONFIG_HOME" "$XDG_DATA_HOME"
|
||||
export HOME XDG_CONFIG_HOME XDG_DATA_HOME
|
||||
|
||||
rm -f Makefile.harness harness
|
||||
qmake harness.pro -o Makefile.harness >/dev/null
|
||||
|
||||
+75
-12
@@ -123,17 +123,74 @@ int main(int argc, char *argv[])
|
||||
check("chiave last_production_update_v1", root.contains("last_production_update_v1"));
|
||||
}
|
||||
|
||||
// --- backup / ripristino ----------------------------------------------
|
||||
// --- backup nel formato dell'app Android -------------------------------
|
||||
QString exported;
|
||||
{
|
||||
const QString backup = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation))
|
||||
.filePath("backup-test.json");
|
||||
check("export", store.exportTo(backup));
|
||||
check("clearAll", store.clearAll());
|
||||
check("dopo clearAll: vuoto", store.reservations().isEmpty() && store.customers().isEmpty());
|
||||
check("import", store.importFrom(backup));
|
||||
check("dopo import: prenotazioni ripristinate", store.reservations().size() == 1);
|
||||
check("dopo import: clienti ripristinati", store.customers().size() == 1);
|
||||
QFile::remove(backup);
|
||||
exported = store.exportBackup();
|
||||
check("export: percorso restituito", !exported.isEmpty(), exported);
|
||||
check("export: file scritto in Documenti",
|
||||
exported.contains(QStringLiteral("/Documents/")), exported);
|
||||
check("export: file esistente", QFile::exists(exported));
|
||||
|
||||
QFile f(exported);
|
||||
f.open(QIODevice::ReadOnly);
|
||||
const QJsonObject root = QJsonDocument::fromJson(f.readAll()).object();
|
||||
f.close();
|
||||
check("backup: chiave breedSettings (come l'app Android)", root.contains("breedSettings"));
|
||||
check("backup: chiave reservations", root.contains("reservations"));
|
||||
check("backup: chiave customers", root.contains("customers"));
|
||||
check("backup: nessuna chiave interna (breed_settings_v2)",
|
||||
!root.contains("breed_settings_v2"));
|
||||
check("backup: esattamente 3 chiavi", root.size() == 3);
|
||||
}
|
||||
|
||||
// backup prodotto dall'app Android (scritto a mano come lo esporta lei):
|
||||
// deve essere accettato e sostituire i dati.
|
||||
{
|
||||
const QString android = QStringLiteral(
|
||||
"{\n"
|
||||
" \"breedSettings\": [\n"
|
||||
" {\"breedId\": \"livorno\", \"hens\": [\n"
|
||||
" {\"id\": \"h9\", \"name\": \"Gina\", \"birthDate\": \"2024-03-01T00:00:00.000\", \"isLaying\": true}\n"
|
||||
" ]}\n"
|
||||
" ],\n"
|
||||
" \"reservations\": [\n"
|
||||
" {\"id\": \"r9\", \"customerName\": \"Luca\", \"breedId\": \"livorno\", \"eggsRequested\": 6,\n"
|
||||
" \"createdAt\": \"2026-10-01T08:00:00.000\", \"deliveryDate\": \"2026-10-03\", \"isDelivered\": false}\n"
|
||||
" ],\n"
|
||||
" \"customers\": [\n"
|
||||
" {\"id\": \"c9\", \"firstName\": \"Luca\", \"lastName\": \"Bianchi\", \"phoneNumber\": \"333\"}\n"
|
||||
" ]\n"
|
||||
"}\n");
|
||||
check("backup Android riconosciuto valido", store.isValidBackup(android));
|
||||
check("import del backup Android", store.importBackup(android));
|
||||
check("import: razze sostituite", store.breedSettings().size() == 1
|
||||
&& store.breedSettings().at(0).toMap().value("breedId").toString() == "livorno");
|
||||
check("import: gallina annidata letta", store.breedSettings().at(0).toMap()
|
||||
.value("hens").toList().at(0).toMap().value("name").toString() == "Gina");
|
||||
check("import: prenotazioni sostituite", store.reservations().size() == 1
|
||||
&& store.reservations().at(0).toMap().value("eggsRequested").toInt() == 6);
|
||||
check("import: clienti sostituiti", store.customers().size() == 1
|
||||
&& store.customers().at(0).toMap().value("lastName").toString() == "Bianchi");
|
||||
|
||||
// il backup Android non porta scorte ne' ultima produzione: si tengono
|
||||
check("import: magazzino preservato", !store.warehouseStock().isEmpty());
|
||||
check("import: data aggiornamento preservata",
|
||||
store.lastProductionUpdate() == "2026-10-09");
|
||||
|
||||
// rilettura da disco: l'import e' stato salvato
|
||||
DataStore reopened;
|
||||
check("import: salvato su disco", reopened.reservations().size() == 1
|
||||
&& reopened.breedSettings().at(0).toMap().value("breedId").toString() == "livorno");
|
||||
}
|
||||
|
||||
// backup senza 'customers' (le versioni vecchie non lo avevano)
|
||||
{
|
||||
const QString old = QStringLiteral("{\"breedSettings\": [], \"reservations\": []}");
|
||||
check("backup senza clienti: valido", store.isValidBackup(old));
|
||||
check("backup senza clienti: importato", store.importBackup(old));
|
||||
check("backup senza clienti: liste vuote", store.breedSettings().isEmpty()
|
||||
&& store.customers().isEmpty());
|
||||
}
|
||||
|
||||
// --- file corrotto: non deve crashare e non deve perdere i dati ---------
|
||||
@@ -147,10 +204,16 @@ int main(int argc, char *argv[])
|
||||
check("file corrotto: non crasha", true);
|
||||
check("file corrotto: stato vuoto", broken.reservations().isEmpty());
|
||||
|
||||
// il backup di un file corrotto viene rifiutato
|
||||
check("import di un file non valido rifiutato", !broken.importFrom(store.dataFilePath()));
|
||||
check("import di un JSON non valido rifiutato", !broken.importBackup("{ rotto"));
|
||||
check("import di un oggetto senza le chiavi richieste rifiutato",
|
||||
!broken.importBackup("{\"altro\": 1}"));
|
||||
check("import di un array (non oggetto) rifiutato", !broken.importBackup("[]"));
|
||||
check("import di testo vuoto rifiutato", !broken.importBackup(" "));
|
||||
check("isValidBackup su testo vuoto", !broken.isValidBackup(""));
|
||||
}
|
||||
|
||||
QFile::remove(exported);
|
||||
|
||||
qInfo().noquote() << (failures == 0 ? "\ntutti i controlli superati"
|
||||
: QString("\n%1 controlli falliti").arg(failures));
|
||||
return failures == 0 ? 0 : 1;
|
||||
|
||||
@@ -9,3 +9,24 @@ node tests/engine.test.js
|
||||
echo
|
||||
echo "== traduzioni e catalogo =="
|
||||
node tests/i18n.test.js
|
||||
echo
|
||||
echo "== interfaccia QML (controlli statici) =="
|
||||
python3 tools/check_qml.py
|
||||
echo
|
||||
echo "== sintassi QML (qmllint, se il Qt host c'e') =="
|
||||
# qmllint senza il modulo Silica controlla SOLO la sintassi, ma proprio gli
|
||||
# errori di sintassi (es. `id x` senza i due punti) fanno fallire la pagina
|
||||
# sul device con "Impossibile caricare la pagina": meglio vederli qui.
|
||||
QTDIR=${QTDIR:-/home/kaneda/Qt/5.15.2/gcc_64}
|
||||
if [ -x "$QTDIR/bin/qmllint" ]; then
|
||||
PATH="$QTDIR/bin:$PATH"
|
||||
export PATH
|
||||
find qml -name '*.qml' | while read -r f; do
|
||||
# si scartano solo le righe sui moduli mancanti (Silica qui non c'e')
|
||||
qmllint "$f" 2>&1 | grep -vE "is not installed|Cannot find|^Warnings occurred|^$" \
|
||||
| sed "s|^|$f: |" || true
|
||||
done
|
||||
echo "qmllint: nessun errore di sintassi"
|
||||
else
|
||||
echo "qmllint non disponibile: controllo saltato"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Controlli statici sulla QML di harbour-henflow.
|
||||
|
||||
La UI Silica non e' eseguibile qui (serve l'SDK Sailfish), ma quasi tutti gli
|
||||
errori che si vedono sul device sono verificabili PRIMA di consegnare:
|
||||
* chiavi di traduzione inesistenti (schermata con la chiave grezza a video)
|
||||
* funzioni del motore JS chiamate ma non esistenti (TypeError a runtime)
|
||||
* pagine aperte con pageStack ma senza file (pagina che non si apre)
|
||||
* componenti Silica/QtQuick inesistenti (\"Impossibile caricare la pagina\")
|
||||
* parentesi non bilanciate (l'intero file QML non compila)
|
||||
|
||||
Uso: python3 tools/check_qml.py # esce 1 se trova problemi
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
QML_DIR = os.path.join(ROOT, "qml")
|
||||
JS_DIR = os.path.join(QML_DIR, "js")
|
||||
|
||||
# Componenti disponibili: Silica 1.0 (elenco ufficiale) + QtQuick/JS di base.
|
||||
SILICA = """
|
||||
AddAnimation ApplicationWindow BackgroundItem BusyIndicator BusyLabel Button ButtonLayout
|
||||
Clipboard ColorPicker ColorPickerDialog ColorPickerPage ColumnView ComboBox ContextMenu Cover
|
||||
CoverAction CoverActionList CoverBackground CoverPlaceholder DatePicker DatePickerDialog DetailItem
|
||||
Dialog DialogHeader DockedPanel Drawer EnterKey FadeAnimation FadeAnimator FirstTimeUseCounter
|
||||
GridItem HighlightImage HorizontalScrollDecorator Icon IconButton IconTextSwitch InteractionHintLabel
|
||||
Keypad Label LinkedLabel ListItem MenuItem MenuLabel Notices OpacityRampEffect Page PageBusyIndicator
|
||||
PagedView PageHeader PageStack Palette PasswordField ProgressBar PullDownMenu PushUpMenu Remorse
|
||||
RemorseItem RemorsePopup RemoveAnimation Screen ScrollDecorator SearchField SectionHeader Separator
|
||||
SilicaControl SilicaFlickable SilicaGridView SilicaItem SilicaListView Slider SlideshowView
|
||||
StandardPaths Switch TapInteractionHint TextArea TextField TextSwitch Theme TimePicker TimePickerDialog
|
||||
TouchBlocker TouchInteractionHint ValueButton VerticalScrollDecorator ViewPlaceholder
|
||||
""".split()
|
||||
|
||||
QTQUICK = """
|
||||
AnchorChanges AnchorAnimation Behavior Binding BorderImage Canvas Column Connections Component
|
||||
DoubleValidator Easing Enumerators Flipable Flow FontLoader FontMetrics Grid Image IntValidator
|
||||
Item ItemGradient KeyNavigation Keys LayoutMirroring ListElement ListModel ListView Loader
|
||||
MouseArea NumberAnimation NumberAnimationOnRotation ParallelAnimation ParentAnimation ParentChange
|
||||
Path PathAnimation PauseAnimation PointDirection PropertyAction PropertyAnimation PropertyChanges
|
||||
Rectangle Repeater RotationAnimation Row ScaleAnimator ScriptAction SequentialAnimation Shape
|
||||
SmoothedAnimation SpringAnimation State StateGroup StateChangeScript StateMachine Text TextEdit
|
||||
TextInput TextMetrics Timer Transition Translate VectorImage ViewTransition XAnimator YAnimator
|
||||
NumberAnimator OpacityAnimator RegExpValidator ScrollView SplitView StackView TabBar TabView
|
||||
ToolBar ToolButton GroupBox CheckBox RadioButton Slider SpinBox DialogButtonBox
|
||||
""".split()
|
||||
|
||||
KNOWN = set(SILICA) | set(QTQUICK)
|
||||
|
||||
|
||||
def strip_strings(src):
|
||||
"""Rimuove stringhe e commenti (per bilanciamento e per i controlli sui tipi)."""
|
||||
out = []
|
||||
i, n = 0, len(src)
|
||||
while i < n:
|
||||
c = src[i]
|
||||
if c == "/" and i + 1 < n and src[i + 1] == "/":
|
||||
while i < n and src[i] != "\n":
|
||||
i += 1
|
||||
elif c == "/" and i + 1 < n and src[i + 1] == "*":
|
||||
i += 2
|
||||
while i + 1 < n and not (src[i] == "*" and src[i + 1] == "/"):
|
||||
i += 1
|
||||
i += 2
|
||||
elif c in "\"'":
|
||||
quote = c
|
||||
i += 1
|
||||
while i < n and src[i] != quote:
|
||||
i += 2 if src[i] == "\\" else 1
|
||||
i += 1
|
||||
out.append('""')
|
||||
else:
|
||||
out.append(c)
|
||||
i += 1
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def js_symbols(path):
|
||||
"""Simboli di primo livello esportati da un modulo JS (var e function)."""
|
||||
src = open(path, encoding="utf-8").read()
|
||||
names = set(re.findall(r"^function\s+(\w+)", src, re.M))
|
||||
names |= set(re.findall(r"^var\s+(\w+)", src, re.M))
|
||||
names |= set(re.findall(r"^ (\w+):\s", src, re.M)) # voci di module.exports
|
||||
return names
|
||||
|
||||
|
||||
def i18n_languages(path):
|
||||
"""{lingua: insieme di chiavi} dal modulo i18n.js."""
|
||||
src = open(path, encoding="utf-8").read()
|
||||
m = re.search(r"var HF_STRINGS = (\{.*?\n\});", src, re.S)
|
||||
if not m:
|
||||
raise SystemExit("check_qml: HF_STRINGS non trovato in i18n.js")
|
||||
data = json.loads(m.group(1))
|
||||
return {lang: set(keys) for lang, keys in data.items()}
|
||||
|
||||
|
||||
def qml_files():
|
||||
found = []
|
||||
for base, _dirs, files in os.walk(QML_DIR):
|
||||
for f in files:
|
||||
if f.endswith(".qml"):
|
||||
found.append(os.path.join(base, f))
|
||||
return sorted(found)
|
||||
|
||||
|
||||
def main():
|
||||
problems = []
|
||||
files = qml_files()
|
||||
if not files:
|
||||
raise SystemExit("check_qml: nessun file QML trovato")
|
||||
|
||||
hf = js_symbols(os.path.join(JS_DIR, "henflow.js"))
|
||||
cat = js_symbols(os.path.join(JS_DIR, "catalog.js"))
|
||||
i18n = js_symbols(os.path.join(JS_DIR, "i18n.js"))
|
||||
langs = i18n_languages(os.path.join(JS_DIR, "i18n.js"))
|
||||
all_keys = set.intersection(*langs.values()) if langs else set()
|
||||
|
||||
for path in files:
|
||||
rel = os.path.relpath(path, ROOT)
|
||||
raw = open(path, encoding="utf-8").read()
|
||||
src = strip_strings(raw)
|
||||
|
||||
# 1. bilanciamento parentesi
|
||||
for opener, closer in (("{", "}"), ("(", ")"), ("[", "]")):
|
||||
if src.count(opener) != src.count(closer):
|
||||
problems.append("%s: %s%s non bilanciate (%d vs %d)"
|
||||
% (rel, opener, closer, src.count(opener), src.count(closer)))
|
||||
|
||||
# 2. riferimenti ai moduli JS
|
||||
for mod, symbols, spelled in (("HF", hf, "henflow.js"),
|
||||
("Catalog", cat, "catalog.js"),
|
||||
("I18N", i18n, "i18n.js")):
|
||||
for name in sorted(set(re.findall(r"\b%s\.(\w+)" % mod, src))):
|
||||
if name not in symbols:
|
||||
problems.append("%s: %s.%s non esiste in %s" % (rel, mod, name, spelled))
|
||||
|
||||
# 3. chiavi di traduzione usate
|
||||
for key in sorted(set(re.findall(r'\b(?:I18N\.t|tr)\(\s*"([A-Za-z0-9_]+)"', raw))):
|
||||
if key not in all_keys:
|
||||
problems.append("%s: chiave di traduzione mancante: %s" % (rel, key))
|
||||
|
||||
# 4. pagine aperte con pageStack
|
||||
for target in sorted(set(re.findall(r'Qt\.resolvedUrl\(\s*"([^"]+\.qml)"', raw))):
|
||||
if not os.path.exists(os.path.join(os.path.dirname(path), target)):
|
||||
problems.append("%s: pagina inesistente: %s" % (rel, target))
|
||||
|
||||
# 5. componenti sconosciuti (Toast & co.)
|
||||
for name in sorted(set(re.findall(r"^\s*([A-Z][A-Za-z0-9_]*)\s*\{", src, re.M))):
|
||||
if name not in KNOWN:
|
||||
problems.append("%s: componente non riconosciuto: %s" % (rel, name))
|
||||
|
||||
# 6. le chiavi devono esistere in TUTTE le lingue
|
||||
for lang, keys in sorted(langs.items()):
|
||||
missing = all_keys - keys
|
||||
if missing:
|
||||
problems.append("i18n.js: chiavi assenti in '%s': %s"
|
||||
% (lang, ", ".join(sorted(missing))))
|
||||
|
||||
print("check_qml: %d file QML, %d chiavi di traduzione, %d lingue"
|
||||
% (len(files), len(all_keys), len(langs)))
|
||||
if problems:
|
||||
print("\nPROBLEMI (%d):" % len(problems))
|
||||
for p in problems:
|
||||
print(" - " + p)
|
||||
return 1
|
||||
print("nessun problema trovato")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user