commit 045df22d5bfa1ca023360fd73a9768348a88ddab Author: mashiros <490328928@qq.com> Date: Fri Apr 8 17:22:11 2022 +0800 initial push diff --git a/Images/Weather/Cloudy.png b/Images/Weather/Cloudy.png new file mode 100644 index 0000000..bac0eb9 Binary files /dev/null and b/Images/Weather/Cloudy.png differ diff --git a/Images/Weather/Fog.png b/Images/Weather/Fog.png new file mode 100644 index 0000000..c2d2859 Binary files /dev/null and b/Images/Weather/Fog.png differ diff --git a/Images/Weather/HeavyRain.png b/Images/Weather/HeavyRain.png new file mode 100644 index 0000000..2aa2c52 Binary files /dev/null and b/Images/Weather/HeavyRain.png differ diff --git a/Images/Weather/HeavySnow.png b/Images/Weather/HeavySnow.png new file mode 100644 index 0000000..3083cdb Binary files /dev/null and b/Images/Weather/HeavySnow.png differ diff --git a/Images/Weather/LightRain.png b/Images/Weather/LightRain.png new file mode 100644 index 0000000..94b6cdc Binary files /dev/null and b/Images/Weather/LightRain.png differ diff --git a/Images/Weather/LightSleet.png b/Images/Weather/LightSleet.png new file mode 100644 index 0000000..1e69b8a Binary files /dev/null and b/Images/Weather/LightSleet.png differ diff --git a/Images/Weather/LightSnow.png b/Images/Weather/LightSnow.png new file mode 100644 index 0000000..8f1b644 Binary files /dev/null and b/Images/Weather/LightSnow.png differ diff --git a/Images/Weather/PartlyCloudy.png b/Images/Weather/PartlyCloudy.png new file mode 100644 index 0000000..e899d30 Binary files /dev/null and b/Images/Weather/PartlyCloudy.png differ diff --git a/Images/Weather/Showers.png b/Images/Weather/Showers.png new file mode 100644 index 0000000..62e9b83 Binary files /dev/null and b/Images/Weather/Showers.png differ diff --git a/Images/Weather/SnowShowers.png b/Images/Weather/SnowShowers.png new file mode 100644 index 0000000..fe18c65 Binary files /dev/null and b/Images/Weather/SnowShowers.png differ diff --git a/Images/Weather/Sunny.png b/Images/Weather/Sunny.png new file mode 100644 index 0000000..751fcff Binary files /dev/null and b/Images/Weather/Sunny.png differ diff --git a/Images/Weather/ThunderyHeavyRain.png b/Images/Weather/ThunderyHeavyRain.png new file mode 100644 index 0000000..8450939 Binary files /dev/null and b/Images/Weather/ThunderyHeavyRain.png differ diff --git a/Images/Weather/ThunderyShowers.png b/Images/Weather/ThunderyShowers.png new file mode 100644 index 0000000..4f92558 Binary files /dev/null and b/Images/Weather/ThunderyShowers.png differ diff --git a/Images/Weather/Unknown.png b/Images/Weather/Unknown.png new file mode 100644 index 0000000..6377c6a Binary files /dev/null and b/Images/Weather/Unknown.png differ diff --git a/Images/Weather/VeryCloudy.png b/Images/Weather/VeryCloudy.png new file mode 100644 index 0000000..8279259 Binary files /dev/null and b/Images/Weather/VeryCloudy.png differ diff --git a/Presets/TopUI/preset.json b/Presets/TopUI/preset.json new file mode 100644 index 0000000..6a5aed6 --- /dev/null +++ b/Presets/TopUI/preset.json @@ -0,0 +1,4 @@ +{ + "source": "nvg://ordinalscale.widget.mashiros.top/widget/topui", + "settings": "settings.xml" +} \ No newline at end of file diff --git a/Presets/TopUI/preview.png b/Presets/TopUI/preview.png new file mode 100644 index 0000000..fb75285 Binary files /dev/null and b/Presets/TopUI/preview.png differ diff --git a/Presets/TopUI/settings.xml b/Presets/TopUI/settings.xml new file mode 100644 index 0000000..aa2bda9 --- /dev/null +++ b/Presets/TopUI/settings.xml @@ -0,0 +1,4 @@ + + + {"Circle Color":"#fffcf9","Line Color":"#fffcf9","Line Width":38,"Shadow Color":"#e0e0e0","Shadow Size":0.5,"Battle UI":true,"Clock Visible":true,"Full Clock":true,"Font Color":"#f5f5f5","Font Size":44,"Font Name":0,"Font Weight":0,"Text Vertical Offset":16} + \ No newline at end of file diff --git a/Previews/BottomUI.png b/Previews/BottomUI.png new file mode 100644 index 0000000..e1ea886 Binary files /dev/null and b/Previews/BottomUI.png differ diff --git a/Previews/TopUI.png b/Previews/TopUI.png new file mode 100644 index 0000000..fcd121f Binary files /dev/null and b/Previews/TopUI.png differ diff --git a/Previews/Weather.png b/Previews/Weather.png new file mode 100644 index 0000000..aca1ec2 Binary files /dev/null and b/Previews/Weather.png differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..7f5017c --- /dev/null +++ b/package.json @@ -0,0 +1,77 @@ +{ + "name": "top.mashiros.widget.ordinalscale", + "version": "1.0.0", + + "title": { + "en": "Ordinal Scale Widget Collections", + "zh": "序列之争挂件合集" + }, + + "description": { + "en": "A collection of Ordinal Scale widget for SAO Utils 2, including top line, bottom line and weather widget with rich configuration options.", + "zh": "用于SAO Utils 2的序列之争挂件合集,包含顶部、底部线与天气挂件,并提供丰富的配置选项。" + }, + + "homepage": "https://nvg.dev/Mashiro_Sorata/ordinal-scale-widgets", + + "bugs": { + "email": "mashiro_sorata@qq.com", + "url": "https://nvg.dev/Mashiro_Sorata/ordinal-scale-widgets/issues" + }, + + + "author": { + "name": "Mashiro_Sorata", + "email": "mashiro_sorata@qq.com", + "url": "http://www.mashiros.top/" + }, + + "engines": { + "qt": "~5", + "qt.quick": ">=2.12", + "nvg.api": "~1" + }, + + "resources": [ + { + "location": "/widget/topui", + "catalog": "widget", + "title": { + "en": "Ordinal Scale Top UI", + "zh": "序列之争顶部UI" + }, + "preview": "Previews/TopUI.png", + "entry": "qml/TopUI.qml" + }, + { + "location": "/widget/bottomui", + "catalog": "widget", + "title": { + "en": "Ordinal Scale Bottom UI", + "zh": "序列之争底部UI" + }, + "preview": "Previews/BottomUI.png", + "entry": "qml/BottomUI.qml" + }, + { + "location": "/widget/weather", + "catalog": "widget", + "title": { + "en": "Ordinal Scale Weather Widget", + "zh": "序列之争天气挂件" + }, + "preview": "Previews/Weather.png", + "entry": "qml/WeatherWidget.qml" + }, + { + "location": "/preset/widget/topui", + "catalog": "preset/widget", + "title": { + "en": "Ordinal Scale Top Battle UI", + "zh": "序列之争顶部战斗UI" + }, + "preview": "Presets/TopUI/preview.png", + "entry": "Presets/TopUI/preset.json" + } + ] +} \ No newline at end of file diff --git a/qml/BottomUI.qml b/qml/BottomUI.qml new file mode 100644 index 0000000..68cf5b9 --- /dev/null +++ b/qml/BottomUI.qml @@ -0,0 +1,208 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import NERvGear 1.0 as NVG +import NERvGear.Controls 1.0 +import NERvGear.Templates 1.0 as T +import NERvGear.Preferences 1.0 as P + +T.Widget { + id: widget + solid: true + visible: true + title: qsTr("Ordinal Scale Bottom UI widget") + + property real thour: 0 + property real t12hour: 0 + property real tmin: 0 + editing: styleDialog.active + + readonly property var configs: widget.settings.styles ? widget.settings.styles : {"Circle Color":"#fffcf9","Line Color":"#fffcf9","Line Width":38,"Shadow Color":"#e0e0e0","Shadow Size":0.5,"Battle UI":false,"Clock Visible":true,"Full Clock":true,"Font Color":"#f5f5f5","Font Size":44,"Font Name":0,"Font Weight":0,"Text Vertical Offset":16} + + property string line_color: configs["Line Color"] + property string shadowColor: configs["Shadow Color"] + property real shadowBlur: configs["Shadow Size"] + + readonly property real h: Math.min(widget.width, widget.height) + readonly property real w: widget.width + readonly property real r: (w**2+4*h**2)/2/h + + property real triangle_size: 24 + + Canvas { + id: triangle + anchors.centerIn: parent + anchors.verticalCenterOffset: height/2 + width: triangle_size + height: triangle_size*Math.sin(Math.PI/6) + contextType: "2d" + onPaint: { + context.reset(); + context.clearRect(0,0,width,height); + context.shadowBlur = shadowBlur; + context.shadowColor = shadowColor; + context.lineWidth = Math.max(0.08*configs["Line Width"] - shadowBlur, 0.1); + context.strokeStyle = line_color; + context.beginPath(); + context.moveTo(0,0); + context.lineTo(width/2, height); + context.lineTo(width, 0); + context.lineTo(0, 0); + context.fillStyle = line_color; + context.fill(); + } + } + + Canvas { + id: line + anchors.centerIn: parent + width: widget.width + height: widget.height + contextType: "2d" + onPaint: { + context.reset(); + context.clearRect(0,0,width,height); + context.shadowBlur = shadowBlur; + context.shadowColor = shadowColor; + context.lineWidth = Math.max(0.08*configs["Line Width"] - shadowBlur, 0.1); + context.strokeStyle = line_color; + let deg = Math.asin(w/2/r)*0.95; + context.beginPath(); + context.arc(w/2, r+h/2, r-shadowBlur/2, deg+Math.PI*3/2, -deg+Math.PI*3/2, true); + context.stroke(); + } + } + + menu: Menu { + Action { + text: qsTr("Settings") + "..." + onTriggered: styleDialog.active = true + } + } + + Loader { + id: styleDialog + active: false + sourceComponent: NVG.Window { + id: window + title: qsTr("Settings") + visible: true + minimumWidth: 380 + minimumHeight: 500 + width: minimumWidth + height: minimumHeight + + transientParent: widget.NVG.View.window + + property var configuration + + Page { + id: cfg_page + anchors.fill: parent + + header: TitleBar { + text: qsTr("UI Settings") + + standardButtons: Dialog.Save | Dialog.Reset + + onAccepted: { + configuration = rootPreference.save(); + widget.settings.styles = configuration; + styleDialog.active = false; + } + + onReset: { + rootPreference.load(); + let cfg = rootPreference.save(); + widget.settings.styles = cfg; + line.requestPaint(); + triangle.requestPaint(); + } + } + + ColumnLayout { + id: root + anchors.fill: parent + anchors.margins: 16 + anchors.topMargin: 0 + + Flickable { + Layout.fillWidth: true + Layout.fillHeight: true + + clip: true + contentWidth: preferenceLayout.implicitWidth + contentHeight: preferenceLayout.implicitHeight + + ColumnLayout { + id: preferenceLayout + width: root.width + + P.PreferenceGroup { + id: rootPreference + Layout.fillWidth: true + + label: qsTr("Configuration") + + onPreferenceEdited: { + widget.settings.styles = rootPreference.save(); + line.requestPaint(); + triangle.requestPaint(); + } + + P.ColorPreference { + name: "Line Color" + label: qsTr("Line Color") + defaultValue: "#fffcf9" + } + + P.SliderPreference { + name: "Line Width" + label: qsTr("Line Width") + from: 1 + to: 100 + stepSize: 1 + defaultValue: 38 + displayValue: value + "%" + } + + P.ColorPreference { + name: "Shadow Color" + label: qsTr("Shadow Color") + defaultValue: "#e0e0e0" + } + + P.SliderPreference { + name: "Shadow Size" + label: qsTr("Shadow Size") + from: 0 + to: 3 + stepSize: 0.1 + defaultValue: 0.5 + displayValue: Math.round(value*10)/10 + "px" + } + + Component.onCompleted: { + if(!widget.settings.styles) { + configuration = rootPreference.save(); + widget.settings.styles = configuration; + } + rootPreference.load(widget.settings.styles); + configuration = widget.settings.styles; + } + } + } + } + } + } + + onClosing: { + widget.settings.styles = configuration; + styleDialog.active = false; + line.requestPaint(); + triangle.requestPaint(); + } + } + } +} diff --git a/qml/TopUI.qml b/qml/TopUI.qml new file mode 100644 index 0000000..4a6632f --- /dev/null +++ b/qml/TopUI.qml @@ -0,0 +1,520 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import NERvGear 1.0 as NVG +import NERvGear.Controls 1.0 +import NERvGear.Templates 1.0 as T +import NERvGear.Preferences 1.0 as P + +T.Widget { + id: widget + solid: true + visible: true + title: qsTr("Ordinal Scale Top UI widget") + + property real thour: 0 + property real t12hour: 0 + property real tmin: 0 + editing: styleDialog.active + + readonly property var configs: widget.settings.styles ? widget.settings.styles : {"Circle Color":"#fffcf9","Line Color":"#fffcf9","Line Width":38,"Shadow Color":"#e0e0e0","Shadow Size":0.5,"Battle UI":false,"Clock Visible":true,"Full Clock":true,"Font Color":"#f5f5f5","Font Size":44,"Font Name":0,"Font Weight":0,"Text Vertical Offset":16} + + property string circle_color: configs["Circle Color"] + property string line_color: configs["Line Color"] + property string shadowColor: configs["Shadow Color"] + property real shadowBlur: configs["Shadow Size"] + readonly property real size: 155 + + readonly property real h: Math.min(widget.width, widget.height) + readonly property real w: widget.width + readonly property real r: (w**2+h**2)/4/h + + readonly property var fonts: Qt.fontFamilies() + readonly property var fontweight: [Font.Light, Font.Normal, Font.Bold] + readonly property var sfontweight: [qsTr("Light"), qsTr("Normal"), qsTr("Bold")] + + Timer { + interval: 250 + running: text_clock.visible + repeat: true + onTriggered: { + var now = new Date(); + tmin = now.getMinutes(); + thour = now.getHours(); + if (!configs["Full Clock"]) + t12hour = thour > 12 ? thour - 12 : thour; + } + } + + Text { + id: text_clock + anchors.top: parent.top + anchors.topMargin: widget.height/200*configs["Text Vertical Offset"] + anchors.horizontalCenter: parent.horizontalCenter + color: configs["Font Color"] + text: configs["Full Clock"] ? ("0"+thour).slice(-2) + ":" + ("0"+tmin).slice(-2) : ("0"+t12hour).slice(-2) + ":" + ("0"+tmin).slice(-2) + font.pointSize: widget.height/200*configs["Font Size"] + font.family: fonts[configs["Font Name"]] + font.weight: fontweight[configs["Font Weight"]] + visible: widget.NVG.View.exposed && !configs["Battle UI"] && configs["Clock Visible"] + } + + Item { + id: circle + anchors.centerIn: parent + anchors.verticalCenterOffset: -2.5 + scale: widget.height/size/1.25 + visible: widget.NVG.View.exposed && configs["Battle UI"] + + Canvas { + id: c1 + anchors.centerIn: parent + width: size + height: size + contextType: "2d" + renderTarget: Canvas.FramebufferObject + renderStrategy: Canvas.Cooperative + onPaint: { + context.reset(); + context.clearRect(0,0,size,size); + context.shadowBlur = shadowBlur; + context.shadowColor = shadowColor; + context.lineWidth = size*0.04 - shadowBlur; + context.beginPath(); + context.arc(size/2, size/2, size*0.38-shadowBlur/2, -Math.PI/6, -Math.PI/6+Math.PI*2/3 , true); + context.strokeStyle = circle_color; + context.stroke(); + } + rotation: -140 + SequentialAnimation on rotation { + running: circle.visible + loops: Animation.Infinite + RotationAnimation { + duration: 1250 + easing.type: Easing.InOutCubic + from: -140 + to: 220 + } + RotationAnimation { + duration: 1250 + easing.type: Easing.InOutCubic + from: 220 + to: -140 + direction: RotationAnimation.Counterclockwise + } + } + } + + Canvas { + id: c2 + anchors.centerIn: parent + width: size + height: size + contextType: "2d" + renderTarget: Canvas.FramebufferObject + renderStrategy: Canvas.Cooperative + onPaint: { + context.reset(); + context.clearRect(0,0,size,size); + context.shadowBlur = shadowBlur; + context.shadowColor = shadowColor; + context.lineWidth = size*0.08 - shadowBlur; + context.beginPath(); + context.arc(size/2, size/2, size*0.30-shadowBlur/2, -Math.PI/6, -Math.PI/24+Math.PI*2/3 , true); + context.strokeStyle = circle_color; + context.stroke(); + } + rotation: 20 + NumberAnimation on rotation { + duration: 2800 + easing.type: Easing.Linear + from: 20 + to: 380 + loops: Animation.Infinite + running: circle.visible + } + } + + Canvas { + id: c3 + anchors.centerIn: parent + width: size + height: size + contextType: "2d" + renderTarget: Canvas.FramebufferObject + renderStrategy: Canvas.Cooperative + onPaint: { + context.reset(); + context.clearRect(0,0,size,size); + context.shadowBlur = shadowBlur; + context.shadowColor = shadowColor; + context.lineWidth = size*0.03 - shadowBlur; + context.beginPath(); + context.arc(size/2, size/2, size*0.23-shadowBlur/2, -Math.PI/6, Math.PI/12+Math.PI*2/3 , true); + context.strokeStyle = circle_color; + context.stroke(); + } + rotation: 90 + NumberAnimation on rotation { + duration: 2000 + easing.type: Easing.Linear + from: 90 + to: -270 + loops: Animation.Infinite + running: circle.visible + } + } + + Canvas { + id: c4 + anchors.centerIn: parent + width: size + height: size + contextType: "2d" + renderTarget: Canvas.FramebufferObject + renderStrategy: Canvas.Cooperative + onPaint: { + context.reset(); + context.clearRect(0,0,size,size); + context.shadowBlur = shadowBlur; + context.shadowColor = shadowColor; + context.lineWidth = size*0.06 - shadowBlur; + context.beginPath(); + context.arc(size/2, size/2, size*0.17-shadowBlur/2, -Math.PI/6, Math.PI/4+Math.PI*2/3 , true); + context.strokeStyle = circle_color; + context.stroke(); + } + rotation: -70 + SequentialAnimation on rotation { + running: circle.visible + loops: Animation.Infinite + RotationAnimation { + duration: 1150 + easing.type: Easing.InOutCubic + from: -70 + to: 290 + } + RotationAnimation { + duration: 1150 + easing.type: Easing.InOutCubic + from: 290 + to: -70 + direction: RotationAnimation.Counterclockwise + } + } + } + + Canvas { + id: c5 + anchors.centerIn: parent + width: size + height: size + contextType: "2d" + renderTarget: Canvas.FramebufferObject + renderStrategy: Canvas.Cooperative + onPaint: { + context.reset(); + context.clearRect(0,0,size,size); + context.shadowBlur = shadowBlur; + context.shadowColor = shadowColor; + context.lineWidth = size*0.1 - shadowBlur; + context.beginPath(); + context.arc(size/2, size/2, size*0.05-shadowBlur/2, 0, -Math.PI*2 , true); + context.strokeStyle = circle_color; + context.stroke(); + } + } + + Canvas { + id: c0 + anchors.centerIn: parent + width: size + height: size + contextType: "2d" + renderTarget: Canvas.FramebufferObject + renderStrategy: Canvas.Cooperative + onPaint: { + context.reset(); + context.clearRect(0,0,size,size); + context.shadowBlur = shadowBlur; + context.shadowColor = shadowColor; + context.lineWidth = size*0.16 - shadowBlur; + context.beginPath(); + context.arc(size/2, size/2, size*0.38-shadowBlur/2, 0, -Math.PI/3 , true); + context.strokeStyle = circle_color; + context.stroke(); + } + + NumberAnimation on rotation { + duration: 1500 + easing.type: Easing.Linear + from: 0 + to: 360 + loops: Animation.Infinite + running: circle.visible + } + } + } + + Canvas { + id: line + anchors.centerIn: parent + width: widget.width + height: widget.height + contextType: "2d" + onPaint: { + context.reset(); + context.clearRect(0,0,width,height); + context.shadowBlur = shadowBlur; + context.shadowColor = shadowColor; + context.lineWidth = Math.max(0.08*configs["Line Width"] - shadowBlur, 0.1); + context.strokeStyle = line_color; + let deg = Math.asin(w/2/r)*0.95; + context.beginPath(); + if (circle.visible) { + context.arc(w/2, -r+h/2, r-shadowBlur/2, deg+Math.PI/2, Math.PI/2+circle.scale*size/1.22/r, true); + context.stroke(); + context.beginPath(); + context.arc(w/2, -r+h/2, r-shadowBlur/2, -deg+Math.PI/2, Math.PI/2-circle.scale*size/1.22/r, false); + context.stroke(); + } else { + context.arc(w/2, -r+h/2, r-shadowBlur/2, deg+Math.PI/2, -deg+Math.PI/2, true); + context.stroke(); + } + } + } + + menu: Menu { + Action { + text: qsTr("Settings") + "..." + onTriggered: styleDialog.active = true + } + } + + Loader { + id: styleDialog + active: false + sourceComponent: NVG.Window { + id: window + title: qsTr("Settings") + visible: true + minimumWidth: 380 + minimumHeight: 500 + width: minimumWidth + height: minimumHeight + + transientParent: widget.NVG.View.window + + property var configuration + + Page { + id: cfg_page + anchors.fill: parent + + header: TitleBar { + text: qsTr("UI Settings") + + standardButtons: Dialog.Save | Dialog.Reset + + onAccepted: { + configuration = rootPreference.save(); + widget.settings.styles = configuration; + styleDialog.active = false; + } + + onReset: { + rootPreference.load(); + let cfg = rootPreference.save(); + widget.settings.styles = cfg; + line.requestPaint(); + c0.requestPaint(); + c1.requestPaint(); + c2.requestPaint(); + c3.requestPaint(); + c4.requestPaint(); + c5.requestPaint(); + } + } + + ColumnLayout { + id: root + anchors.fill: parent + anchors.margins: 16 + anchors.topMargin: 0 + + Flickable { + Layout.fillWidth: true + Layout.fillHeight: true + + clip: true + contentWidth: preferenceLayout.implicitWidth + contentHeight: preferenceLayout.implicitHeight + + ColumnLayout { + id: preferenceLayout + width: root.width + + P.PreferenceGroup { + id: rootPreference + Layout.fillWidth: true + + label: qsTr("Configuration") + + onPreferenceEdited: { + widget.settings.styles = rootPreference.save(); + line.requestPaint(); + c0.requestPaint(); + c1.requestPaint(); + c2.requestPaint(); + c3.requestPaint(); + c4.requestPaint(); + c5.requestPaint(); + } + + P.ColorPreference { + name: "Circle Color" + label: qsTr("Circle Color") + defaultValue: "#fffcf9" + } + + P.ColorPreference { + name: "Line Color" + label: qsTr("Line Color") + defaultValue: "#fffcf9" + } + + P.SliderPreference { + name: "Line Width" + label: qsTr("Line Width") + from: 1 + to: 100 + stepSize: 1 + defaultValue: 38 + displayValue: value + "%" + } + + P.ColorPreference { + name: "Shadow Color" + label: qsTr("Shadow Color") + defaultValue: "#e0e0e0" + } + + P.SliderPreference { + name: "Shadow Size" + label: qsTr("Shadow Size") + from: 0 + to: 3 + stepSize: 0.1 + defaultValue: 0.5 + displayValue: Math.round(value*10)/10 + "px" + } + + P.Separator {} + + P.SwitchPreference { + id: _cfg_battle_ui + name: "Battle UI" + label: qsTr("Battle UI") + defaultValue: false + } + + P.Separator {} + + P.SwitchPreference { + id: _cfg_clock_visible + name: "Clock Visible" + label: qsTr("Clock Visible") + visible: !_cfg_battle_ui.value + enabled: visible + defaultValue: true + } + + P.SwitchPreference { + name: "Full Clock" + label: qsTr("24 Hour Clock") + visible: !_cfg_battle_ui.value + enabled: visible && _cfg_clock_visible.value + defaultValue: true + } + + P.ColorPreference { + name: "Font Color" + label: qsTr("Font Color") + visible: !_cfg_battle_ui.value + enabled: visible && _cfg_clock_visible.value + defaultValue: "#f5f5f5" + } + + P.SliderPreference { + name: "Font Size" + label: qsTr("Font Size") + visible: !_cfg_battle_ui.value + enabled: visible && _cfg_clock_visible.value + from: 1 + to: 100 + stepSize: 1 + defaultValue: 44 + displayValue: value + "%" + } + + P.SelectPreference { + name: "Font Name" + label: qsTr("Font Style") + visible: !_cfg_battle_ui.value + enabled: visible && _cfg_clock_visible.value + icon.name: "solid:\uf1fc" + defaultValue: 0 + model: fonts + } + + P.SelectPreference { + name: "Font Weight" + label: qsTr("Font Weight") + visible: !_cfg_battle_ui.value + enabled: visible && _cfg_clock_visible.value + icon.name: "solid:\uf1fc" + defaultValue: 0 + model: sfontweight + } + + P.SliderPreference { + name: "Text Vertical Offset" + label: qsTr("Text Vertical Offset") + visible: !_cfg_battle_ui.value + enabled: visible && _cfg_clock_visible.value + from: -100 + to: 100 + stepSize: 1 + defaultValue: 16 + displayValue: value + "%" + } + + Component.onCompleted: { + if(!widget.settings.styles) { + configuration = rootPreference.save(); + widget.settings.styles = configuration; + } + rootPreference.load(widget.settings.styles); + configuration = widget.settings.styles; + } + } + } + } + } + } + + onClosing: { + widget.settings.styles = configuration; + styleDialog.active = false; + line.requestPaint(); + c0.requestPaint(); + c1.requestPaint(); + c2.requestPaint(); + c3.requestPaint(); + c4.requestPaint(); + c5.requestPaint(); + } + } + } +} diff --git a/qml/WeatherWidget.qml b/qml/WeatherWidget.qml new file mode 100644 index 0000000..34c522d --- /dev/null +++ b/qml/WeatherWidget.qml @@ -0,0 +1,509 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import QtQuick.Shapes 1.1 + +import QtGraphicalEffects 1.12 + +import NERvGear 1.0 as NVG +import NERvGear.Controls 1.0 +import NERvGear.Templates 1.0 as T +import NERvGear.Preferences 1.0 as P + +import "utils.js" as Utils + +T.Widget { + id: widget + + title: qsTr("Weather Widget") + solid: true + + readonly property var fonts: Qt.fontFamilies() + readonly property var fontweight: [Font.Light, Font.Normal, Font.Bold] + readonly property var sfontweight: [qsTr("Light"), qsTr("Normal"), qsTr("Bold")] + + readonly property var configs: widget.settings.styles ?? {"Location":"","Display Location":"","Update Interval":{"Value":1,"Unit":1},"Background Color":"#ffa502","Background Opacity":60,"Area Opacity Difference":17,"Icon Color":"#fefefe","Temperature Text Settings":{"Font Color":"#f5f5f5","Font Size":90,"Font Name":fonts.length-1,"Font Weight":1,"X Offset":33,"Y Offset":32},"Area Text Settings":{"Font Color":"#f5f5f5","Font Size":60,"Font Name":fonts.length-1,"Font Weight":1,"X Offset":0,"Y Offset":-56,"Border Margin":40}} + + readonly property real w: widget.width + readonly property real h: 0.46*widget.width + + Item { + id: main + width: w + height: h + anchors.centerIn: parent + layer.enabled: true + layer.effect: OpacityMask{ + maskSource: Rectangle { + width: w + height: h + color: "black" + radius: h/15 + } + } + + Rectangle { + id: weather_box + width: w + height: h + anchors.left: parent.left + anchors.top: parent.top + opacity: configs["Background Opacity"]/100 + color: configs["Background Color"] + } + + Item { + id: sdialog + width: h/8 + height: h/8 + anchors.left: parent.left + anchors.leftMargin: h/16 + anchors.topMargin: h/14 + anchors.top: parent.top + + Shape { + anchors.fill: parent + ShapePath { + strokeWidth: parent.height/120 + strokeColor: configs["Icon Color"] + + startX: h/44; startY: h/800 + PathLine { x: h*0.10227272727272727; y: h/800 } + } + + ShapePath { + strokeWidth: parent.height/120 + strokeColor: configs["Icon Color"] + + startX: h/44; startY: h/800 + h/32 + PathLine { x: h*0.10227272727272727; y: h/800 + h/32 } + } + + ShapePath { + strokeWidth: parent.height/120 + strokeColor: configs["Icon Color"] + + startX: h/44; startY: h/800 + h/16 + PathLine { x: h*0.10227272727272727; y: h/800 + h/16 } + } + } + + MouseArea { + anchors.fill: parent + enabled: !styleDialog.active + onClicked: { + styleDialog.active = true; + } + } + } + + Item { + id: weather + width: 0.655*w + height: h + anchors.left: parent.left + anchors.top: parent.top + + Image { + id: weather_mask + anchors.centerIn: weather + autoTransform: true + visible: false + source: "../Images/Weather/Unknown.png" + } + + Rectangle { + id: wcolor + width: h/1.1 + height: h/1.1 + anchors.centerIn: weather + visible: false + color: configs["Icon Color"] + } + + OpacityMask { + anchors.fill: wcolor + source: wcolor + maskSource: weather_mask + } + } + + Rectangle { + id: temper_box + width: 0.345*w + height: h + anchors.right: parent.right + anchors.top: parent.top + opacity: configs["Background Opacity"]*configs["Area Opacity Difference"]/10000 + color: "black" + } + + Text { + id: area + anchors.centerIn: temper_box + anchors.horizontalCenterOffset: temper_box.width*configs["Area Text Settings"]["X Offset"]/200 + anchors.verticalCenterOffset: h*configs["Area Text Settings"]["Y Offset"]/200 + color: configs["Area Text Settings"]["Font Color"] + text: "" + font.pixelSize: w*0.0009*configs["Area Text Settings"]["Font Size"] + font.family: fonts[configs["Area Text Settings"]["Font Name"]] + font.weight: fontweight[configs["Area Text Settings"]["Font Weight"]] + + Rectangle { + anchors.fill: parent + anchors.margins: -area.font.pixelSize*configs["Area Text Settings"]["Border Margin"]/100 + color: "transparent" + border.color: configs["Area Text Settings"]["Font Color"] + border.width: area.font.pixelSize/15 + radius: area.font.pixelSize/3.5 + visible: Boolean(area.text) + } + } + + Text { + id: temperature + anchors.centerIn: temper_box + anchors.horizontalCenterOffset: temper_box.width*configs["Temperature Text Settings"]["X Offset"]/200 + anchors.verticalCenterOffset: h*configs["Temperature Text Settings"]["Y Offset"]/200 + color: configs["Temperature Text Settings"]["Font Color"] + text: "--°" + font.pixelSize: w*0.002*configs["Temperature Text Settings"]["Font Size"] + font.family: fonts[configs["Temperature Text Settings"]["Font Name"]] + font.weight: fontweight[configs["Temperature Text Settings"]["Font Weight"]] + } + } + + function setWeather() { + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if(xhr.readyState === XMLHttpRequest.DONE) { + let data = xhr.responseText.toString(); + if (data) { + let jdata = JSON.parse(data); + temperature.text = jdata["current_condition"][0]["temp_C"] + "°"; + if (!configs["Display Location"] && !configs["Location"]) { + area.text = jdata["nearest_area"][0]["areaName"][0]["value"]; + } + weather_mask.source = "../Images/Weather/" + Utils.weather_codes[jdata["current_condition"][0]["weatherCode"]] ?? "Unknown" + ".png"; + } else { + weather_mask.source = "../Images/Weather/Unknown.png"; + } + } + } + if (configs["Location"]) + xhr.open("GET", 'https://wttr.in/'+configs["Location"]+'?format=j1'); + else + xhr.open("GET", 'https://wttr.in/?format=j1'); + xhr.send(); + if (configs["Display Location"]) { + area.text = configs["Display Location"]; + } else if (configs["Location"]) { + area.text = configs["Location"] + } + } + + Timer { + id: timer + interval: 600000 + running: widget.NVG.View.exposed + repeat: true + triggeredOnStart: true + onTriggered: { + setWeather(); + } + } + + menu: Menu { + Action { + text: qsTr("Settings") + "..." + onTriggered: styleDialog.active = true + } + + Action { + text: qsTr("Refresh") + onTriggered: timer.restart() + } + } + + Loader { + id: styleDialog + active: false + sourceComponent: NVG.Window { + id: window + title: qsTr("Settings") + visible: true + minimumWidth: 450 + minimumHeight: 550 + width: minimumWidth + height: minimumHeight + + transientParent: widget.NVG.View.window + + property var configuration + + Page { + id: cfg_page + anchors.fill: parent + + header: TitleBar { + text: qsTr("Settings") + + standardButtons: Dialog.Save | Dialog.Reset + + onAccepted: { + configuration = rootPreference.save(); + widget.settings.styles = configuration; + styleDialog.active = false; + timer.interval = 60000*configs["Update Interval"]["Value"]*(1+59*configs["Update Interval"]["Unit"]) + setWeather(); + } + + onReset: { + rootPreference.load(); + let cfg = rootPreference.save(); + widget.settings.styles = cfg; + } + } + + ColumnLayout { + id: root + anchors.fill: parent + anchors.margins: 16 + anchors.topMargin: 0 + + Flickable { + Layout.fillWidth: true + Layout.fillHeight: true + + clip: true + contentWidth: preferenceLayout.implicitWidth + contentHeight: preferenceLayout.implicitHeight + + ColumnLayout { + id: preferenceLayout + width: root.width + + P.PreferenceGroup { + id: rootPreference + Layout.fillWidth: true + + label: qsTr("Configuration") + + onPreferenceEdited: { + widget.settings.styles = rootPreference.save(); + } + + P.TextFieldPreference { + name: "Location" + label: qsTr("Location") + message: qsTr("Search address by location, latitude and longitude.") + } + + P.TextFieldPreference { + name: "Display Location" + label: qsTr("Display Location") + message: "The location to display in widget." + } + + P.DialogPreference { + name: "Update Interval" + label: qsTr("Update Interval") + live: true + displayValue: _cfg_update_interval_value.value + " " + [qsTr("Minutes"), qsTr("Hours")][_cfg_update_interval_unit.value] + + P.SpinPreference { + id: _cfg_update_interval_value + name: "Value" + from: 1 + to: 1440 + editable: true + defaultValue: 1 + } + + P.SelectPreference { + id: _cfg_update_interval_unit + name: "Unit" + label: qsTr("Unit") + defaultValue: 1 + model: [qsTr("Minutes"), qsTr("Hours")] + } + } + + P.ColorPreference { + name: "Background Color" + label: qsTr("Background Color") + defaultValue: "#ffa502" + } + + P.SliderPreference { + name: "Background Opacity" + label: qsTr("Background Opacity") + from: 0 + to: 100 + stepSize: 1 + defaultValue: 60 + displayValue: value + "%" + } + + P.SliderPreference { + name: "Area Opacity Difference" + label: qsTr("Area Opacity Difference") + from: 0 + to: 100 + stepSize: 1 + defaultValue: 17 + displayValue: value + "%" + } + + P.ColorPreference { + name: "Icon Color" + label: qsTr("Icon Color") + defaultValue: "#fefefe" + } + + P.DialogPreference { + name: "Temperature Text Settings" + label: qsTr("Temperature Text Settings") + icon.name: "solid:\uf1fc" + live: true + + P.ColorPreference { + name: "Font Color" + label: qsTr("Font Color") + defaultValue: "#f5f5f5" + } + + P.SliderPreference { + name: "Font Size" + label: qsTr("Font Size") + from: 1 + to: 100 + stepSize: 1 + defaultValue: 90 + displayValue: value + "%" + } + + P.SelectPreference { + name: "Font Name" + label: qsTr("Font Style") + defaultValue: fonts.length-1 + model: fonts + } + + P.SelectPreference { + name: "Font Weight" + label: qsTr("Font Weight") + defaultValue: 1 + model: sfontweight + } + + P.SliderPreference { + name: "X Offset" + label: qsTr("X Offset") + from: -100 + to: 100 + stepSize: 1 + defaultValue: 33 + displayValue: value + "%" + } + + P.SliderPreference { + name: "Y Offset" + label: qsTr("Y Offset") + from: -100 + to: 100 + stepSize: 1 + defaultValue: 32 + displayValue: value + "%" + } + } + + P.DialogPreference { + name: "Area Text Settings" + label: qsTr("Area Text Settings") + icon.name: "solid:\uf1fc" + live: true + + P.ColorPreference { + name: "Font Color" + label: qsTr("Font Color") + defaultValue: "#f5f5f5" + } + + P.SliderPreference { + name: "Font Size" + label: qsTr("Font Size") + from: 1 + to: 100 + stepSize: 1 + defaultValue: 60 + displayValue: value + "%" + } + + P.SelectPreference { + name: "Font Name" + label: qsTr("Font Style") + defaultValue: fonts.length-1 + model: fonts + } + + P.SelectPreference { + name: "Font Weight" + label: qsTr("Font Weight") + defaultValue: 1 + model: sfontweight + } + + P.SliderPreference { + name: "X Offset" + label: qsTr("X Offset") + from: -100 + to: 100 + stepSize: 1 + defaultValue: 0 + displayValue: value + "%" + } + + P.SliderPreference { + name: "Y Offset" + label: qsTr("Y Offset") + from: -100 + to: 100 + stepSize: 1 + defaultValue: -56 + displayValue: value + "%" + } + + P.SliderPreference { + name: "Border Margin" + label: qsTr("Border Margin") + from: 0 + to: 100 + stepSize: 1 + defaultValue: 40 + displayValue: value + "%" + } + } + + Component.onCompleted: { + if(!widget.settings.styles) { + configuration = rootPreference.save(); + widget.settings.styles = configuration; + } + rootPreference.load(widget.settings.styles); + configuration = widget.settings.styles; + } + } + } + } + } + } + + onClosing: { + widget.settings.styles = configuration; + styleDialog.active = false; + } + } + } +} diff --git a/qml/utils.js b/qml/utils.js new file mode 100644 index 0000000..9d32aad --- /dev/null +++ b/qml/utils.js @@ -0,0 +1,53 @@ +.pragma library + +// https://github.com/chubin/wttr.in/blob/master/lib/constants.py +var weather_codes = { + "113": "Sunny", + "116": "PartlyCloudy", + "119": "Cloudy", + "122": "VeryCloudy", + "143": "Fog", + "176": "Showers", + "179": "LightSleet", + "182": "LightSleet", + "185": "LightSleet", + "200": "ThunderyShowers", + "227": "LightSnow", + "230": "HeavySnow", + "248": "Fog", + "260": "Fog", + "263": "Showers", + "266": "LightRain", + "281": "LightSleet", + "284": "LightSleet", + "293": "LightRain", + "296": "LightRain", + "299": "Showers", + "302": "HeavyRain", + "305": "Showers", + "308": "HeavyRain", + "311": "LightSleet", + "314": "LightSleet", + "317": "LightSleet", + "320": "LightSnow", + "323": "SnowShowers", + "326": "SnowShowers", + "329": "HeavySnow", + "332": "HeavySnow", + "335": "SnowShowers", + "338": "HeavySnow", + "350": "LightSleet", + "353": "Showers", + "356": "Showers", + "359": "HeavyRain", + "362": "LightSleet", + "365": "LightSleet", + "368": "SnowShowers", + "371": "SnowShowers", + "374": "LightSleet", + "377": "LightSleet", + "386": "ThunderyShowers", + "389": "ThunderyHeavyRain", + "392": "SnowShowers", + "395": "SnowShowers" +}; \ No newline at end of file