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:
2026-10-09 09:21:47 +02:00
parent a73d94a74e
commit 248e0db899
25 changed files with 2060 additions and 74 deletions
+65
View File
@@ -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 {}
}
}
+130
View File
@@ -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 {}
}
}
+257
View File
@@ -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 {}
}
}
+206
View File
@@ -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)
}
}
+152
View File
@@ -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 {}
}
}
+119
View File
@@ -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 {}
}
}
+128
View File
@@ -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 {}
}
}
+261
View File
@@ -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 {}
}
}
+74
View File
@@ -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 {}
}
}
+81
View File
@@ -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
View File
@@ -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 })
}
}
+1 -1
View File
@@ -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
}
+96
View File
@@ -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 {}
}
}