harbour-maree 0.1.0: motore armonico 7 costituenti, 16 porti, ricerca libera (Nominatim+locale), grafico 24/48/72h, EN/IT, preferenze (lingua, porto, unità, intervallo), icone harbour
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
import QtQuick 2.0
|
||||
import Sailfish.Silica 1.0
|
||||
import "../js/tides.js" as Tides
|
||||
|
||||
Page {
|
||||
id: page
|
||||
|
||||
property var port
|
||||
property int portIndex: -1
|
||||
property bool custom: false
|
||||
property int days: appSettings.days
|
||||
property string unit: appSettings.unit
|
||||
property var nearPort: null
|
||||
property real nearKm: 0
|
||||
|
||||
ListModel { id: eventsModel }
|
||||
|
||||
Timer {
|
||||
interval: 600000 // aggiorna ogni 10 minuti
|
||||
repeat: true
|
||||
running: true
|
||||
onTriggered: refresh()
|
||||
}
|
||||
|
||||
onPortChanged: init()
|
||||
|
||||
function init() {
|
||||
nearPort = null
|
||||
nearKm = 0
|
||||
if (custom && port.length > 5 && port[5] !== "") {
|
||||
var np = Tides.nearestPort(port[2], port[3])
|
||||
if (np.port !== null) {
|
||||
nearPort = np.port
|
||||
nearKm = np.km
|
||||
}
|
||||
}
|
||||
refresh()
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (!port) return
|
||||
// eventi
|
||||
eventsModel.clear()
|
||||
var ex = Tides.extrema(port, new Date(), days)
|
||||
var n = Math.min(ex.length, 6)
|
||||
for (var i = 0; i < n; i++) {
|
||||
eventsModel.append({
|
||||
kind: ex[i].kind,
|
||||
date: Tides.fmtDate(ex[i].t),
|
||||
time: Tides.fmtHM(ex[i].t),
|
||||
height: Tides.fmtH(ex[i].h, unit)
|
||||
})
|
||||
}
|
||||
// astro
|
||||
var mp = Tides.moonPhase(new Date())
|
||||
moonLabel.text = mp.icon + " " + moonNames[mp.idx] + " (" + qsTr("age %1 d").arg(mp.age.toFixed(1)) + ")"
|
||||
var st = Tides.sunTimes(port[2], new Date())
|
||||
sunLabel.text = Tides.fmtHM(new Date(0, 0, 0, st.rise, 0)) + " / " + Tides.fmtHM(new Date(0, 0, 0, st.set, 0)) + " " + qsTr("(solar time)")
|
||||
rangeLabel.text = "±" + ((port[4][1][0] + port[4][2][0]) * 2).toFixed(2) + " m"
|
||||
// info curva
|
||||
var f = port[5] || ""
|
||||
if (f.indexOf("near:") === 0) {
|
||||
var p = f.split(":")
|
||||
curveInfo.text = qsTr("Constants of the port of %1 (distance %2°)").arg(p[1]).arg(p[2])
|
||||
} else if (f === "synth") {
|
||||
curveInfo.text = qsTr("Estimated constants from lat/lon (synthetic model)")
|
||||
} else {
|
||||
curveInfo.text = qsTr("Z₀ %1 m · 7 harmonic constituents").arg(port[4][0].toFixed(2))
|
||||
}
|
||||
cv.requestPaint()
|
||||
}
|
||||
|
||||
property var moonNames: [
|
||||
qsTr("New moon"), qsTr("Waxing crescent"), qsTr("First quarter"), qsTr("Waxing gibbous"),
|
||||
qsTr("Full moon"), qsTr("Waning gibbous"), qsTr("Last quarter"), qsTr("Waning crescent")
|
||||
]
|
||||
|
||||
SilicaFlickable {
|
||||
id: flick
|
||||
anchors.fill: parent
|
||||
contentHeight: col.height
|
||||
|
||||
Column {
|
||||
id: col
|
||||
width: parent.width
|
||||
|
||||
PageHeader {
|
||||
title: page.port ? page.port[0] : ""
|
||||
description: page.port
|
||||
? (page.port[1] !== "" ? page.port[1] + " · " : "")
|
||||
+ qsTr("lat %1°").arg(page.port[2].toFixed(2))
|
||||
: ""
|
||||
}
|
||||
|
||||
// Card porto più vicino (solo città cercate) — rosso
|
||||
Rectangle {
|
||||
id: nearCard
|
||||
visible: page.custom && page.nearPort !== null
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
height: visible ? Theme.itemSizeSmall : 0
|
||||
radius: Theme.paddingSmall
|
||||
color: "transparent"
|
||||
border.width: 2
|
||||
border.color: Theme.errorColor
|
||||
|
||||
Label {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.paddingSmall
|
||||
text: qsTr("📍 Nearest port: %1 — %2 · ±%3 m")
|
||||
.arg(page.nearPort[0])
|
||||
.arg(page.nearKm < 222
|
||||
? (page.nearKm / 111).toFixed(1) + "°"
|
||||
: Math.round(page.nearKm) + " km")
|
||||
.arg(((page.nearPort[4][1][0] + page.nearPort[4][2][0]) * 2).toFixed(2))
|
||||
color: Theme.errorColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.Wrap
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("DetailPage.qml"),
|
||||
{ port: page.nearPort,
|
||||
portIndex: Tides.PORTS.indexOf(page.nearPort),
|
||||
custom: false })
|
||||
}
|
||||
}
|
||||
|
||||
// Intervallo (chips 24/48/72)
|
||||
Row {
|
||||
spacing: Theme.paddingMedium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Repeater {
|
||||
model: [1, 2, 3]
|
||||
delegate: Rectangle {
|
||||
width: 110
|
||||
height: Theme.itemSizeSmall
|
||||
radius: Theme.paddingMedium
|
||||
color: page.days === modelData ? Theme.highlightColor : Theme.rgba(Theme.highlightBackgroundColor, 0.2)
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: modelData === 1 ? qsTr("24 h") : (modelData === 2 ? qsTr("48 h") : qsTr("72 h"))
|
||||
color: page.days === modelData ? Theme.primaryColor : Theme.secondaryColor
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
page.days = modelData
|
||||
appSettings.days = modelData
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingMedium }
|
||||
|
||||
// Grafico
|
||||
Rectangle {
|
||||
id: cvBox
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
height: 250
|
||||
color: "transparent"
|
||||
|
||||
Canvas {
|
||||
id: cv
|
||||
anchors.fill: parent
|
||||
onPaint: draw()
|
||||
onWidthChanged: requestPaint()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cvMouse
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
var t = page.port ? Tides.tide(page.port, new Date(nowBase + (mouse.x / width) * (page.days * 86400000))) : 0
|
||||
var d = new Date(nowBase + (mouse.x / width) * (page.days * 86400000))
|
||||
tip.text = Tides.fmtDate(d.getTime()) + " " + Tides.fmtHM(d.getTime()) + " → " + Tides.fmtH(t, page.unit)
|
||||
tip.visible = true
|
||||
tipTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
property double nowBase: new Date().getTime()
|
||||
|
||||
Label {
|
||||
id: tip
|
||||
visible: false
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Theme.paddingSmall
|
||||
color: Theme.highlightColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
background: Rectangle { color: Theme.rgba(Theme.primaryColor, 0.1); radius: Theme.paddingSmall }
|
||||
}
|
||||
Timer { id: tipTimer; interval: 2500; onTriggered: tip.visible = false }
|
||||
}
|
||||
|
||||
Label {
|
||||
id: curveInfo
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingMedium }
|
||||
|
||||
SectionHeader { text: qsTr("Next high and low tides") }
|
||||
|
||||
Repeater {
|
||||
model: eventsModel
|
||||
delegate: Item {
|
||||
width: parent.width
|
||||
height: Theme.itemSizeSmall
|
||||
Label {
|
||||
text: model.kind === "high" ? "▲ " + qsTr("High") : "▼ " + qsTr("Low")
|
||||
color: model.kind === "high" ? Theme.highlightColor : Theme.errorColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.horizontalPageMargin
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Label {
|
||||
text: model.date + " " + model.time
|
||||
color: Theme.primaryColor
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.horizontalPageMargin + 100
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Label {
|
||||
text: model.height
|
||||
color: Theme.primaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.horizontalPageMargin
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader { text: qsTr("Astro") }
|
||||
|
||||
Item { width: parent.width; height: Theme.itemSizeSmall
|
||||
Label { text: "🌙 " + qsTr("Moon"); color: Theme.secondaryColor; anchors.left: parent.left; anchors.leftMargin: Theme.horizontalPageMargin; anchors.verticalCenter: parent.verticalCenter }
|
||||
Label { id: moonLabel; anchors.right: parent.right; anchors.rightMargin: Theme.horizontalPageMargin; anchors.verticalCenter: parent.verticalCenter }
|
||||
}
|
||||
Item { width: parent.width; height: Theme.itemSizeSmall
|
||||
Label { text: "☀️ " + qsTr("Sunrise / sunset"); color: Theme.secondaryColor; anchors.left: parent.left; anchors.leftMargin: Theme.horizontalPageMargin; anchors.verticalCenter: parent.verticalCenter }
|
||||
Label { id: sunLabel; anchors.right: parent.right; anchors.rightMargin: Theme.horizontalPageMargin; anchors.verticalCenter: parent.verticalCenter; font.pixelSize: Theme.fontSizeSmall }
|
||||
}
|
||||
Item { width: parent.width; height: Theme.itemSizeSmall
|
||||
Label { text: qsTr("Typical range"); color: Theme.secondaryColor; anchors.left: parent.left; anchors.leftMargin: Theme.horizontalPageMargin; anchors.verticalCenter: parent.verticalCenter }
|
||||
Label { id: rangeLabel; anchors.right: parent.right; anchors.rightMargin: Theme.horizontalPageMargin; anchors.verticalCenter: parent.verticalCenter }
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingLarge }
|
||||
}
|
||||
}
|
||||
|
||||
// disegno della curva (stesso algoritmo del mockup)
|
||||
function draw() {
|
||||
if (!port || cv.width < 10) return
|
||||
var ctx = cv.getContext("2d")
|
||||
ctx.clearRect(0, 0, cv.width, cv.height)
|
||||
var now = new Date()
|
||||
var span = days * 86400000
|
||||
var w = cv.width, h = cv.height
|
||||
var pad = 12
|
||||
var steps = Math.round(span / (10 * 60000))
|
||||
var min = 1e9, max = -1e9, i, t, ht
|
||||
var pts = []
|
||||
for (i = 0; i <= steps; i++) {
|
||||
t = now.getTime() + i * span / steps
|
||||
ht = Tides.tide(port, new Date(t))
|
||||
pts.push([t, ht])
|
||||
if (ht < min) min = ht
|
||||
if (ht > max) max = ht
|
||||
}
|
||||
min -= 0.05; max += 0.05
|
||||
function X(t) { return pad + (t - now.getTime()) / span * (w - 2 * pad) }
|
||||
function Y(hv) { return h - pad - (hv - min) / (max - min) * (h - 2 * pad) }
|
||||
|
||||
// griglia ogni 6 h
|
||||
ctx.strokeStyle = Theme.rgba(Theme.primaryColor, 0.12)
|
||||
ctx.fillStyle = Theme.rgba(Theme.secondaryColor, 0.8)
|
||||
ctx.font = "10px sans-serif"
|
||||
ctx.lineWidth = 1
|
||||
var gsteps = Math.round(span / 3600000 / 6)
|
||||
for (i = 0; i <= gsteps; i++) {
|
||||
t = now.getTime() + i * 6 * 3600000
|
||||
var x = X(t)
|
||||
ctx.beginPath(); ctx.moveTo(x, pad); ctx.lineTo(x, h - pad); ctx.stroke()
|
||||
var d = new Date(t)
|
||||
ctx.fillText((d.getHours() < 10 ? "0" : "") + d.getHours() + ":00", x + 3, h - 3)
|
||||
}
|
||||
|
||||
// Z0 tratteggiata
|
||||
ctx.setLineDash([4, 4])
|
||||
ctx.strokeStyle = Theme.rgba(Theme.secondaryColor, 0.6)
|
||||
ctx.beginPath(); ctx.moveTo(pad, Y(port[4][0])); ctx.lineTo(w - pad, Y(port[4][0])); ctx.stroke()
|
||||
ctx.setLineDash([])
|
||||
|
||||
// curva
|
||||
ctx.strokeStyle = Theme.highlightColor
|
||||
ctx.lineWidth = 2
|
||||
ctx.beginPath()
|
||||
for (i = 0; i < pts.length; i++) {
|
||||
if (i === 0) ctx.moveTo(X(pts[i][0]), Y(pts[i][1]))
|
||||
else ctx.lineTo(X(pts[i][0]), Y(pts[i][1]))
|
||||
}
|
||||
ctx.stroke()
|
||||
|
||||
// estremi
|
||||
var ex = Tides.extrema(port, now, days)
|
||||
var shown = 0
|
||||
for (i = 0; i < ex.length && shown < days * 4; i++) {
|
||||
if (ex[i].t < now.getTime() - 60000 || ex[i].t > now.getTime() + span) continue
|
||||
shown++
|
||||
var up = ex[i].kind === "high"
|
||||
ctx.fillStyle = up ? Theme.highlightColor : Theme.errorColor
|
||||
ctx.beginPath(); ctx.arc(X(ex[i].t), Y(ex[i].h), 4, 0, 7); ctx.fill()
|
||||
var d2 = new Date(ex[i].t)
|
||||
var lbl = (d2.getHours() < 10 ? "0" : "") + d2.getHours() + ":" + (d2.getMinutes() < 10 ? "0" : "") + d2.getMinutes()
|
||||
ctx.fillText(lbl, X(ex[i].t) - 16, up ? Y(ex[i].h) - 8 : Y(ex[i].h) + 16)
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: init()
|
||||
|
||||
PullDownMenu {
|
||||
MenuItem {
|
||||
text: qsTr("Set as preferred port")
|
||||
visible: page.port && page.port[0] !== appSettings.preferredPort
|
||||
onClicked: {
|
||||
appSettings.preferredPort = page.port[0]
|
||||
pageStack.replace(Qt.resolvedUrl("MainPage.qml"))
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("Settings")
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import QtQuick 2.0
|
||||
import Sailfish.Silica 1.0
|
||||
import "../js/tides.js" as Tides
|
||||
|
||||
Page {
|
||||
id: page
|
||||
|
||||
property var customPorts: [] // porti creati dalla ricerca libera (sessione)
|
||||
|
||||
SilicaListView {
|
||||
id: list
|
||||
anchors.fill: parent
|
||||
model: ListModel { id: portModel }
|
||||
|
||||
header: Column {
|
||||
width: parent.width
|
||||
PageHeader { title: "Maree" }
|
||||
|
||||
SearchField {
|
||||
id: filter
|
||||
placeholderText: qsTr("Filter ports…")
|
||||
onTextChanged: rebuild()
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingMedium }
|
||||
|
||||
TextField {
|
||||
id: geoField
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
placeholderText: qsTr("Free search: any city (online)…")
|
||||
EnterKey.enabled: text.length > 0
|
||||
EnterKey.onClicked: findCity()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Find")
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
onClicked: findCity()
|
||||
}
|
||||
|
||||
Label {
|
||||
id: geoMsg
|
||||
visible: false
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
wrapMode: Text.Wrap
|
||||
color: Theme.errorColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
|
||||
SectionHeader { text: qsTr("Ports") }
|
||||
}
|
||||
|
||||
delegate: ListItem {
|
||||
contentHeight: Theme.itemSizeMedium
|
||||
highlighted: model.isPreferred
|
||||
|
||||
Label {
|
||||
text: model.name
|
||||
color: model.isPreferred ? Theme.highlightColor : Theme.primaryColor
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.horizontalPageMargin
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
truncationMode: TruncationMode.Fade
|
||||
width: parent.width - 3 * Theme.horizontalPageMargin
|
||||
}
|
||||
Label {
|
||||
text: model.country
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.horizontalPageMargin
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Theme.paddingSmall
|
||||
}
|
||||
Label {
|
||||
text: model.range
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.horizontalPageMargin
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
onClicked: {
|
||||
pageStack.push(Qt.resolvedUrl("DetailPage.qml"),
|
||||
{ port: model.port, portIndex: model.realIndex, custom: model.custom })
|
||||
}
|
||||
}
|
||||
|
||||
VerticalScrollDecorator {}
|
||||
}
|
||||
|
||||
function rebuild() {
|
||||
portModel.clear()
|
||||
var f = filter.text.toLowerCase()
|
||||
var pref = appSettings.preferredPort
|
||||
var i, p
|
||||
for (i = 0; i < Tides.PORTS.length; i++) {
|
||||
p = Tides.PORTS[i]
|
||||
if (f !== "" && (p[0] + " " + p[1]).toLowerCase().indexOf(f) === -1)
|
||||
continue
|
||||
portModel.append({
|
||||
name: p[0], country: p[1],
|
||||
range: "±" + (p[4][1][0] + p[4][2][0]).toFixed(2) + " m",
|
||||
port: p, realIndex: i, custom: false,
|
||||
isPreferred: (pref === p[0])
|
||||
})
|
||||
}
|
||||
for (i = 0; i < customPorts.length; i++) {
|
||||
p = customPorts[i]
|
||||
if (f !== "" && p[0].toLowerCase().indexOf(f) === -1)
|
||||
continue
|
||||
portModel.append({
|
||||
name: p[0], country: qsTr("searched city"),
|
||||
range: "±" + (p[4][1][0] + p[4][2][0]).toFixed(2) + " m",
|
||||
port: p, realIndex: -1, custom: true,
|
||||
isPreferred: (pref === p[0])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- Ricerca libera (geocoding) ---
|
||||
function geoFail() {
|
||||
geoMsg.text = qsTr("City not found (offline and not in local dataset).")
|
||||
geoMsg.visible = true
|
||||
}
|
||||
|
||||
function geoFound(name, lat, lon, src) {
|
||||
if (!name) { geoFail(); return }
|
||||
var np = Tides.nearestPort(lat, lon)
|
||||
var cost, flag
|
||||
if (np.km < 222) {
|
||||
cost = np.port[4].slice()
|
||||
flag = "near:" + np.port[0] + ":" + (np.km / 111).toFixed(1)
|
||||
} else {
|
||||
cost = Tides.synthConst(lat, lon)
|
||||
flag = "synth"
|
||||
}
|
||||
var port = [name, "", lat, lon, cost, flag]
|
||||
customPorts.push(port)
|
||||
rebuild()
|
||||
geoMsg.text = qsTr("Found %1 (via %2)").arg(name).arg(src)
|
||||
geoMsg.visible = true
|
||||
geoMsg.color = Theme.highlightColor
|
||||
pageStack.push(Qt.resolvedUrl("DetailPage.qml"),
|
||||
{ port: port, portIndex: -1, custom: true })
|
||||
}
|
||||
|
||||
function geoXhr(query, cb) {
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.open("GET", "https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q="
|
||||
+ encodeURIComponent(query), true)
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var j = JSON.parse(xhr.responseText)
|
||||
if (j && j.length > 0) {
|
||||
cb(j[0].name || j[0].display_name.split(",")[0],
|
||||
parseFloat(j[0].lat), parseFloat(j[0].lon), "OpenStreetMap")
|
||||
} else {
|
||||
cb(null)
|
||||
}
|
||||
} catch (e) { cb(null) }
|
||||
} else {
|
||||
cb(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.send()
|
||||
}
|
||||
|
||||
function findCity() {
|
||||
var q = geoField.text.trim()
|
||||
if (q === "") return
|
||||
geoMsg.text = qsTr("Searching…")
|
||||
geoMsg.visible = true
|
||||
geoMsg.color = Theme.secondaryColor
|
||||
var loc = Tides.findCityLocal(q)
|
||||
if (loc) {
|
||||
geoFound(loc[0], loc[2], loc[3], qsTr("local dataset"))
|
||||
} else {
|
||||
geoXhr(q, geoFound)
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
rebuild()
|
||||
var pref = appSettings.preferredPort
|
||||
if (pref !== "") {
|
||||
for (var i = 0; i < Tides.PORTS.length; i++) {
|
||||
if (Tides.PORTS[i][0] === pref) {
|
||||
pageStack.push(Qt.resolvedUrl("DetailPage.qml"),
|
||||
{ port: Tides.PORTS[i], portIndex: i, custom: false })
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import QtQuick 2.0
|
||||
import Sailfish.Silica 1.0
|
||||
import "../js/tides.js" as Tides
|
||||
|
||||
Page {
|
||||
id: page
|
||||
|
||||
PageHeader { title: qsTr("Settings") }
|
||||
|
||||
SilicaFlickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: col.height
|
||||
|
||||
Column {
|
||||
id: col
|
||||
width: parent.width
|
||||
|
||||
ComboBox {
|
||||
id: langBox
|
||||
label: qsTr("Language")
|
||||
currentIndex: appSettings.language === "it" ? 2
|
||||
: (appSettings.language === "en" ? 1 : 0)
|
||||
menu: ContextMenu {
|
||||
MenuItem {
|
||||
text: qsTr("System")
|
||||
onClicked: appSettings.language = "system"
|
||||
}
|
||||
MenuItem {
|
||||
text: "English"
|
||||
onClicked: appSettings.language = "en"
|
||||
}
|
||||
MenuItem {
|
||||
text: "Italiano"
|
||||
onClicked: appSettings.language = "it"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Language changes on restart")
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.horizontalPageMargin
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.horizontalPageMargin
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingMedium }
|
||||
|
||||
ComboBox {
|
||||
id: unitBox
|
||||
label: qsTr("Units")
|
||||
currentIndex: appSettings.unit === "ft" ? 1 : 0
|
||||
menu: ContextMenu {
|
||||
MenuItem {
|
||||
text: qsTr("metres")
|
||||
onClicked: appSettings.unit = "m"
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("feet")
|
||||
onClicked: appSettings.unit = "ft"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: daysBox
|
||||
label: qsTr("Default range")
|
||||
currentIndex: Math.max(0, appSettings.days - 1)
|
||||
menu: ContextMenu {
|
||||
MenuItem {
|
||||
text: qsTr("24 h")
|
||||
onClicked: appSettings.days = 1
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("48 h")
|
||||
onClicked: appSettings.days = 2
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("72 h")
|
||||
onClicked: appSettings.days = 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: prefBox
|
||||
label: qsTr("Preferred port")
|
||||
currentIndex: prefIndex()
|
||||
menu: ContextMenu {
|
||||
MenuItem {
|
||||
text: qsTr("None")
|
||||
onClicked: appSettings.preferredPort = ""
|
||||
}
|
||||
Repeater {
|
||||
model: Tides.PORTS.length
|
||||
delegate: MenuItem {
|
||||
text: Tides.PORTS[index][0]
|
||||
onClicked: appSettings.preferredPort = Tides.PORTS[index][0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prefIndex() {
|
||||
var p = appSettings.preferredPort
|
||||
if (p === "") return 0
|
||||
for (var i = 0; i < Tides.PORTS.length; i++)
|
||||
if (Tides.PORTS[i][0] === p) return i + 1
|
||||
return 0
|
||||
}
|
||||
|
||||
SectionHeader { text: qsTr("About") }
|
||||
|
||||
Label {
|
||||
text: "Maree " + appVersion + "\n" + qsTr("Universal tide predictions")
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.horizontalPageMargin
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.horizontalPageMargin
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
Label {
|
||||
text: qsTr("Harmonic model: M2, S2, N2, K1, O1, P1, K2 — known ports or synthetic estimate from lat/lon. Not for navigation.")
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.horizontalPageMargin
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.horizontalPageMargin
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingLarge }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user