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:
@@ -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