diff --git a/.gitignore b/.gitignore
index 582ec9e..fd60946 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,15 @@
*.user*
build/
+build-release/
dist/
__pycache__
rpm/*.rpm
TODO
*~
MANIFEST
+
+icons-src/raw
+icons-src/*/raw
+
+libs/opal-translations
+libs/opal-docs
diff --git a/Makefile b/Makefile
index 8c4c4f7..7c13d3c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,37 +1,71 @@
##########################################################################
# Makefile for SeriesFinale
-
-# This is a helper Makefile to build the python translation files
-# since that is not automatically done with qtcreator.
-# The .ls files are converted to .po files with lconvert. A tool
-# that comes with the Qt distribution.
-# The .po files are compiled to binary .mo format with msgfmt.
+#
+# This is a helper Makefile to build the Python translation files
+# since that is not automatically done with QtCreator.
+#
+# Dependencies:
+# - lconvert: a language file conversion tool that comes with
+# the Qt distribution
+# - pybabel: a Python tool for managing gettext translations,
+# https://pypi.org/project/babel
+# - msgfmt: a tool for compiling gettext translations from .po to .mo,
+# comes with the gettext distribution
##########################################################################
TSFILES = $(wildcard translations/python-messages-*.ts)
-POFILES = $(subst .ts,.po,$(subst python-messages-,,$(TSFILES)))
+POFILES = $(patsubst translations/python-messages-%.ts,translations/%/LC_MESSAGES/python-messages.po, $(TSFILES))
+TSFAKES = $(patsubst translations/python-messages-%.ts,translations/python-messages-%-fake.ts, $(TSFILES))
LOCALEDIR = src/SeriesFinale/locale
-MOFILES = $(patsubst translations/%.po,$(LOCALEDIR)/%/LC_MESSAGES/seriesfinale.mo, $(POFILES))
+MOFILES = $(patsubst translations/%/LC_MESSAGES/python-messages.po,$(LOCALEDIR)/%/LC_MESSAGES/seriesfinale.mo, $(POFILES))
-LCONVERT = /opt/Qt/6.7.0/gcc_64/bin/lconvert
+LCONVERT = /usr/bin/lconvert6
+PYBABEL = pybabel
MSGFMT = msgfmt
.PHONY: help clean
help:
@echo ""
- @echo " make translations Convert and build python translation files"
+ @echo " ** Manage Python translations **"
+ @echo ""
+ @echo " make update Update translation catalogs for translating"
+ @echo " make translations Convert and build Python translation files"
@echo " make clean Remove generated and compiled files"
@echo ""
+ @echo " Note: Qt translations are built automatically when building the app"
+ @echo ""
translations: $(MOFILES)
-translations/%.po: translations/python-messages-%.ts
+translations/%/LC_MESSAGES/python-messages.po: translations/python-messages-%.ts
+ @mkdir -p $(@D)
$(LCONVERT) $< -o $@
-$(LOCALEDIR)/%/LC_MESSAGES/seriesfinale.mo: translations/%.po
+$(LOCALEDIR)/%/LC_MESSAGES/seriesfinale.mo: translations/%/LC_MESSAGES/python-messages.po
@mkdir -p $(@D)
$(MSGFMT) $< -o $@
+ @rm -r $(subst LC_MESSAGES/python-messages.po,,$<)
+
+update-pot:
+ $(PYBABEL) extract \
+ --project "harbour-seriesfinale" \
+ --copyright-holder "harbour-seriesfinale contributors" \
+ -c "TRANSLATORS" --no-wrap \
+ src/SeriesFinale/series.py \
+ -o translations/python-messages.pot
+
+translations/python-messages-%-fake.ts: translations/%/LC_MESSAGES/python-messages.po
+ $(LCONVERT) $< -o $(subst -fake.ts,.ts,$@) \
+ -target-language $(subst -fake.ts,,$(subst translations/python-messages-,,$@)) \
+ -no-obsolete -locations absolute
+ @rm -r $(subst LC_MESSAGES/python-messages.po,,$<)
+
+update-po: update-pot $(POFILES)
+ $(PYBABEL) update --no-wrap -D python-messages -i translations/python-messages.pot -d translations
+
+update: update-po $(TSFAKES)
+ $(LCONVERT) translations/python-messages.pot -o translations/python-messages.ts -drop-translations
clean:
rm -f $(POFILES)
diff --git a/data/seriesfinale.svg b/data/seriesfinale.svg
deleted file mode 100644
index 00cd763..0000000
--- a/data/seriesfinale.svg
+++ /dev/null
@@ -1,171 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/data/seriesfinale_cover.svg b/data/seriesfinale_cover.svg
deleted file mode 100644
index 200a6ca..0000000
--- a/data/seriesfinale_cover.svg
+++ /dev/null
@@ -1,174 +0,0 @@
-
-
-
-
diff --git a/harbour-seriesfinale.desktop b/harbour-seriesfinale.desktop
index e88fdc4..43a00dd 100644
--- a/harbour-seriesfinale.desktop
+++ b/harbour-seriesfinale.desktop
@@ -4,3 +4,16 @@ X-Nemo-Application-Type=silica-qt5
Icon=harbour-seriesfinale
Exec=sailfish-qml harbour-seriesfinale
Name=SeriesFinale
+
+[X-Sailjail]
+Sandboxing=Disabled
+
+## Enabling Sailjail would require migrating data from
+## .local/share/harbour-seriesfinale/ to .local/share/harbour-seriesfinale/harbour-seriesfinale
+# Permissions=Internet
+# OrganizationName=harbour-seriesfinale
+# ApplicationName=harbour-seriesfinale
+
+[X-HarbourBackup]
+BackupPathList=.local/share/harbour-seriesfinale/:.cache/harbour-seriesfinale:.config/harbour-seriesfinale
+BackupConfigList=/apps/harbour-seriesfinale/
diff --git a/harbour-seriesfinale.pro b/harbour-seriesfinale.pro
index 56a0563..ca08c92 100644
--- a/harbour-seriesfinale.pro
+++ b/harbour-seriesfinale.pro
@@ -29,7 +29,7 @@ OTHER_FILES += qml/harbour-seriesfinale.qml \
translations/*.ts \
harbour-seriesfinale.desktop
-SAILFISHAPP_ICONS = 86x86 108x108 128x128 256x256
+SAILFISHAPP_ICONS = 86x86 108x108 128x128 172x172
INSTALLS += src
@@ -37,15 +37,12 @@ INSTALLS += src
# following CONFIG line
CONFIG += sailfishapp_i18n
-# German translation is enabled as an example. If you aren't
-# planning to localize your app, remember to comment out the
-# following TRANSLATIONS line. And also do not forget to
-# modify the localized app name in the the .desktop file.
-TRANSLATIONS += translations/harbour-seriesfinale-de.ts \
- translations/harbour-seriesfinale-es.ts \
- translations/harbour-seriesfinale-sv.ts
+TRANSLATIONS += translations/harbour-seriesfinale-*.ts
DISTFILES += \
+ qml/pages/PrioritySelectionDialog.qml \
+ qml/pages/ShowInfoDialog.qml \
+ qml/pages/components/OpalAboutAttribution.qml \
qml/pages/AboutPage.qml \
qml/pages/AddShow.qml \
qml/pages/EpisodeListRowDelegate.qml \
@@ -56,6 +53,7 @@ DISTFILES += \
qml/pages/SeriesPage.qml \
qml/pages/SettingsPage.qml \
qml/pages/ShowPage.qml \
+ qml/pages/*.qml \
qml/util.js \
src/seriesfinale.py \
src/SeriesFinale/placeholderimage.png \
diff --git a/icons-src/harbour-seriesfinale.svg b/icons-src/harbour-seriesfinale.svg
new file mode 100644
index 0000000..e80e086
--- /dev/null
+++ b/icons-src/harbour-seriesfinale.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/icons-src/placeholderimage.svg b/icons-src/placeholderimage.svg
new file mode 100644
index 0000000..4ed7e02
--- /dev/null
+++ b/icons-src/placeholderimage.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/icons-src/render-icons.sh b/icons-src/render-icons.sh
new file mode 100755
index 0000000..da4e6da
--- /dev/null
+++ b/icons-src/render-icons.sh
@@ -0,0 +1,63 @@
+#!/bin/bash
+#
+# This file is part of Opal and has been released into the public domain.
+# SPDX-License-Identifier: CC0-1.0
+# SPDX-FileCopyrightText: 2021-2023 Mirian Margiani
+#
+# See https://github.com/Pretty-SFOS/opal/blob/main/snippets/opal-render-icons.md
+# for documentation.
+#
+# @@@ keep this line: based on template v1.0.0
+#
+c__FOR_RENDER_LIB__="1.0.0"
+
+# Run this script from the same directory where your icon sources are located,
+# e.g. /icons-src.
+source ../libs/opal-render-icons.sh
+cFORCE=false
+
+for i in raw/*.svg; do
+ if [[ "$i" -nt "${i#raw/}" ]]; then
+ scour "$i" > "${i#raw/}"
+ fi
+done
+
+cMY_APP=harbour-seriesfinale
+
+cNAME="app icons"
+cITEMS=("$cMY_APP")
+cRESOLUTIONS=(86 108 128 172)
+cTARGETS=(../icons/RESXxRESY)
+render_batch
+
+cNAME="status icons"
+
+cITEMS=("placeholderimage")
+cRESOLUTIONS=(400x578)
+cTARGETS=(../src/SeriesFinale)
+render_batch
+
+cITEMS=("$cMY_APP@256")
+cRESOLUTIONS=(F1)
+cTARGETS=(../qml/images)
+render_batch
+
+cITEMS=("seriesfinale_cover")
+cRESOLUTIONS=(204x230)
+cTARGETS=(../qml/images)
+render_batch
+
+# cNAME="store icon"
+# cITEMS=("$cMY_APP")
+# cRESOLUTIONS=(172)
+# cTARGETS=(../dist)
+# render_batch
+
+# cNAME="banner image"
+# cITEMS=(../dist/banner)
+# cRESOLUTIONS=(
+# 1080x540++-large
+# 540x270++-small
+# )
+# cTARGETS=(../dist)
+# render_batch
diff --git a/icons-src/seriesfinale_cover.svg b/icons-src/seriesfinale_cover.svg
new file mode 100644
index 0000000..de82471
--- /dev/null
+++ b/icons-src/seriesfinale_cover.svg
@@ -0,0 +1,36 @@
+
+
+
diff --git a/icons/108x108/harbour-seriesfinale.png b/icons/108x108/harbour-seriesfinale.png
index 0d3323f..0e84f62 100644
Binary files a/icons/108x108/harbour-seriesfinale.png and b/icons/108x108/harbour-seriesfinale.png differ
diff --git a/icons/128x128/harbour-seriesfinale.png b/icons/128x128/harbour-seriesfinale.png
index 4d45810..63ee843 100644
Binary files a/icons/128x128/harbour-seriesfinale.png and b/icons/128x128/harbour-seriesfinale.png differ
diff --git a/icons/172x172/harbour-seriesfinale.png b/icons/172x172/harbour-seriesfinale.png
new file mode 100644
index 0000000..f926415
Binary files /dev/null and b/icons/172x172/harbour-seriesfinale.png differ
diff --git a/icons/256x256/harbour-seriesfinale.png b/icons/256x256/harbour-seriesfinale.png
deleted file mode 100644
index fc94314..0000000
Binary files a/icons/256x256/harbour-seriesfinale.png and /dev/null differ
diff --git a/icons/86x86/harbour-seriesfinale.png b/icons/86x86/harbour-seriesfinale.png
index 3fd38ce..3b7b15c 100644
Binary files a/icons/86x86/harbour-seriesfinale.png and b/icons/86x86/harbour-seriesfinale.png differ
diff --git a/libs/module_opal-about.txt b/libs/module_opal-about.txt
new file mode 100644
index 0000000..c75914b
--- /dev/null
+++ b/libs/module_opal-about.txt
@@ -0,0 +1,21 @@
+# Store this file to keep track of packaged module versions.
+# It is not necessary to ship this in your app's final RPM package.
+# SPDX-FileCopyrightText: 2018-2024 Mirian Margiani
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Attribution using Opal.About:
+# Opal attributions are automatically added since version 3.0.0.
+# Check the "autoAddOpalAttributions" property on your About page.
+#
+# Manual attribution:
+# 1. Import "../modules/Opal/Attributions" in your "About" page.
+# 2. Attribute this module by adding "OpalAboutAttribution {}"
+# to the "attributions" list property of the "About" page.
+
+module: Opal.About (opal-about)
+version: 3.0.0 (git:1110030)
+description: This module provides AboutPageBase for building customizable application information pages.
+maintainers: Mirian Margiani
+attribution: 2018-2024 Mirian Margiani
+license: GPL-3.0-or-later
+sources: https://github.com/Pretty-SFOS/opal-about
diff --git a/libs/module_opal-delegates.txt b/libs/module_opal-delegates.txt
new file mode 100644
index 0000000..dfb128b
--- /dev/null
+++ b/libs/module_opal-delegates.txt
@@ -0,0 +1,21 @@
+# Store this file to keep track of packaged module versions.
+# It is not necessary to ship this in your app's final RPM package.
+# SPDX-FileCopyrightText: 2024-2025 Mirian Margiani (ichthyosaurus):2023 Peter G. (nephros)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Attribution using Opal.About:
+# Opal attributions are automatically added since version 3.0.0.
+# Check the "autoAddOpalAttributions" property on your About page.
+#
+# Manual attribution:
+# 1. Import "../modules/Opal/Attributions" in your "About" page.
+# 2. Attribute this module by adding "OpalDelegatesAttribution {}"
+# to the "attributions" list property of the "About" page.
+
+module: Opal.Delegates (opal-delegates)
+version: 3.5.1 (git:6cac9fd)
+description: This module provides list items for views, so you can concentrate on handling the data, not formatting the presentation.
+maintainers: Mirian Margiani
+attribution: 2024-2025 Mirian Margiani (ichthyosaurus):2023 Peter G. (nephros)
+license: GPL-3.0-or-later
+sources: https://github.com/Pretty-SFOS/opal-delegates
diff --git a/libs/module_opal-linkhandler.txt b/libs/module_opal-linkhandler.txt
new file mode 100644
index 0000000..286fb71
--- /dev/null
+++ b/libs/module_opal-linkhandler.txt
@@ -0,0 +1,21 @@
+# Store this file to keep track of packaged module versions.
+# It is not necessary to ship this in your app's final RPM package.
+# SPDX-FileCopyrightText: 2020-2024 Mirian Margiani
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Attribution using Opal.About:
+# Opal attributions are automatically added since version 3.0.0.
+# Check the "autoAddOpalAttributions" property on your About page.
+#
+# Manual attribution:
+# 1. Import "../modules/Opal/Attributions" in your "About" page.
+# 2. Attribute this module by adding "OpalLinkHandlerAttribution {}"
+# to the "attributions" list property of the "About" page.
+
+module: Opal.LinkHandler (opal-linkhandler)
+version: 2.3.0 (git:dec5c79)
+description: This module provides a link handler to open or copy external links.
+maintainers: Mirian Margiani
+attribution: 2020-2024 Mirian Margiani
+license: GPL-3.0-or-later
+sources: https://github.com/Pretty-SFOS/opal-linkhandler
diff --git a/libs/module_opal-menuswitch.txt b/libs/module_opal-menuswitch.txt
new file mode 100644
index 0000000..e15de65
--- /dev/null
+++ b/libs/module_opal-menuswitch.txt
@@ -0,0 +1,21 @@
+# Store this file to keep track of packaged module versions.
+# It is not necessary to ship this in your app's final RPM package.
+# SPDX-FileCopyrightText: 2024 Mirian Margiani
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Attribution using Opal.About:
+# Opal attributions are automatically added since version 3.0.0.
+# Check the "autoAddOpalAttributions" property on your About page.
+#
+# Manual attribution:
+# 1. Import "../modules/Opal/Attributions" in your "About" page.
+# 2. Attribute this module by adding "OpalMenuSwitchAttribution {}"
+# to the "attributions" list property of the "About" page.
+
+module: Opal.MenuSwitch (opal-menuswitch)
+version: 1.0.1 (git:66ab90b)
+description: This module provides a toggle button to be used in Sailfish-style menus.
+maintainers: Mirian Margiani
+attribution: 2024 Mirian Margiani
+license: GPL-3.0-or-later
+sources: https://github.com/Pretty-SFOS/opal-menuswitch
diff --git a/libs/module_opal-smartscrollbar.txt b/libs/module_opal-smartscrollbar.txt
new file mode 100644
index 0000000..86764a9
--- /dev/null
+++ b/libs/module_opal-smartscrollbar.txt
@@ -0,0 +1,21 @@
+# Store this file to keep track of packaged module versions.
+# It is not necessary to ship this in your app's final RPM package.
+# SPDX-FileCopyrightText: 2024 Mirian Margiani
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Attribution using Opal.About:
+# Opal attributions are automatically added since version 3.0.0.
+# Check the "autoAddOpalAttributions" property on your About page.
+#
+# Manual attribution:
+# 1. Import "../modules/Opal/Attributions" in your "About" page.
+# 2. Attribute this module by adding "OpalSmartScrollbarAttribution {}"
+# to the "attributions" list property of the "About" page.
+
+module: Opal.SmartScrollbar (opal-smartscrollbar)
+version: 1.0.0 (git:a0e025a)
+description: This module provides a Harbour-compatible smart scroll bar for long lists.
+maintainers: Mirian Margiani
+attribution: 2024 Mirian Margiani
+license: GPL-3.0-or-later
+sources: https://github.com/Pretty-SFOS/opal-smartscrollbar
diff --git a/libs/opal-render-icons.sh b/libs/opal-render-icons.sh
new file mode 100644
index 0000000..0199e49
--- /dev/null
+++ b/libs/opal-render-icons.sh
@@ -0,0 +1,197 @@
+#!/bin/bash
+#
+# This file is part of Opal and has been released under the Creative Commons
+# Attribution-ShareAlike 4.0 International License.
+# SPDX-License-Identifier: CC-BY-SA-4.0
+# SPDX-FileCopyrightText: 2018-2023 Mirian Margiani
+#
+# See https://github.com/Pretty-SFOS/opal/blob/main/snippets/opal-render-icons.md
+# for documentation.
+#
+# @@@ FILE VERSION $c__OPAL_RENDER_ICONS_VERSION__
+#
+
+c__OPAL_RENDER_ICONS_VERSION__="1.0.0"
+# c__FOR_RENDER_LIB__=version must be set in module release scripts
+
+shopt -s extglob
+
+cFIELD_INDICATOR="F"
+cFILE_SUFFIX="svg"
+cRESOLUTION_CHECK='^[0-9]+$'
+cDEPENDENCIES=(inkscape pngcrush)
+
+function check_dependencies() {
+ for dep in "${cDEPENDENCIES[@]}"; do
+ if ! which "$dep" 2> /dev/null >&2; then
+ printf "error: %s is required\n" "$dep" >&2
+ exit 1
+ fi
+ done
+}
+
+function log() {
+ IFS=' ' printf -- "%s\n" "$*" >&2
+}
+
+function verify_version() {
+ # @@@ shared function version: 1.1.1
+ local user_version_var="c__FOR_RENDER_LIB__"
+ local opal_version_var="c__OPAL_RENDER_ICONS_VERSION__"
+
+ if [[ -z "${!user_version_var}" ]]; then
+ log "error: script compatibility cannot be verified"
+ log " make sure $user_version_var is set"
+ exit 1
+ fi
+
+ if [[ ! "${!user_version_var}" =~ ^[0-9]+.[0-9]+.[0-9]+$ ]] && [[ ! "${!user_version_var}" =~ ^[0-9]+.[0-9]+.[0-9]+[-+] ]]; then
+ # we don't verify pre-release versions and build metadata (i.e. everything after "-" or "+")
+ log "error: variable $user_version_var='${!user_version_var}' does not contain a valid version number"
+ exit 1
+ fi
+
+ local major="${!user_version_var%%.*}"
+ local minor="${!user_version_var#*.}"; minor="${minor%.*}"
+ # shellcheck disable=SC2034
+ local patch="${!user_version_var##*.}"
+
+ local opal_major="${!opal_version_var%%.*}"
+ local opal_minor="${!opal_version_var#*.}"; opal_minor="${opal_minor%.*}"
+ # shellcheck disable=SC2034
+ local opal_patch="${!opal_version_var##*.}"
+
+ if [[ "$opal_major" == 0 && "$major" == "$opal_major" && "$minor" != "$opal_minor" ]]; then
+ log "module script: ${!user_version_var}, opal library script: ${!opal_version_var}"
+ log "warning: unstable API has changed, please check the script"
+ log " if everything is fine, update $user_version_var"
+ exit 1
+ fi
+
+ if (( "$opal_major" > "$major" )); then
+ log "module script: ${!user_version_var}, opal library script: ${!opal_version_var}"
+ log "error: please update the script for the current major library version ($opal_major vs. $major)"
+ exit 1
+ fi
+
+ if (( "$opal_major" < "$major" || "$opal_minor" < "$minor" )); then
+ log "module script: ${!user_version_var}, opal library script: ${!opal_version_var}"
+ log "warning: the script expects a newer public API ($opal_major.$opal_minor vs. $major.$minor)"
+ log " please update the library"
+ exit 1
+ fi
+}
+
+# make sure script and library are compatible
+verify_version
+
+# check dependencies immediately after loading the script
+# If the user changes cDEPENDENCIES later, they can re-run this command.
+check_dependencies
+
+function do_render_single() { # 1: input, 2: width, 3: height, 4: output
+ printf "rendering %s to %s at %sx%s" "$1" "$4" "$2" "$3"
+ # replace '-o' by '-z -e' for inkscape < 1.0
+ inkscape -o "$4" -w "$2" -h "$3" "$1" && pngcrush -ow "$4"
+}
+
+function split_at_sign() { # 1: string with values separated by @, |, +
+ unset OPAL_SPLIT_RES
+
+ if [[ -n "$2" ]]; then
+ if [[ "$2" =~ ^[@|+]$ ]]; then
+ split="$2"
+ else
+ printf "error: invalid split character '%s'" "$2"
+ exit 255
+ fi
+ else
+ split="@"
+ fi
+
+ mapfile -d $'\0' -t OPAL_SPLIT_RES < <(printf "%s" "$@" | sed "s/\\\\$split/__SIGN_REPLACED__/g;" |\
+ tr "$split" '\0' | sed "s/__SIGN_REPLACED__/$split/g")
+}
+
+function render_batch() { # 1: keep or unset config after rendering?
+ # no arguments required, all info has to be set as variables
+ keep_config="$1" # 'keep' or empty
+
+ printf "rendering %s...\n" "$cNAME"
+ local use_res use_loc source target
+
+ for item in "${cITEMS[@]}"; do
+ split_at_sign "$item" "@"
+ item_split=("${OPAL_SPLIT_RES[@]}")
+ source="${item_split[0]}.$cFILE_SUFFIX"
+
+ if [[ ! -f "$source" ]]; then
+ printf "error: source item '%s' not found\n" "$source"
+ continue
+ fi
+
+ if [[ "$cRESOLUTIONS" == ${cFIELD_INDICATOR}* ]]; then
+ split_at_sign "${item_split[${cRESOLUTIONS#$cFIELD_INDICATOR}]}" "|" # format: res[|res[|...]]
+ use_res=("${OPAL_SPLIT_RES[@]}")
+ else
+ use_res=("${cRESOLUTIONS[@]}")
+ fi
+
+ if [[ "$cTARGETS" == ${cFIELD_INDICATOR}* ]]; then
+ split_at_sign "${item_split[${cTARGETS#$cFIELD_INDICATOR}]}" "|" # format: loc[|loc[|...]]
+ use_loc=("${OPAL_SPLIT_RES[@]}")
+ else
+ use_loc=("${cTARGETS[@]}")
+ fi
+
+ for res in "${use_res[@]}"; do
+ split_at_sign "$res" "+" # format: X[xY][+prefix[+suffix]]
+ res_split=("${OPAL_SPLIT_RES[@]}")
+ local res_x="${res_split[0]}"
+ local res_y="${res_split[0]}"
+
+ if [[ "${res_split[0]}" == *x* ]]; then
+ res_x="${res_split[0]%%x*}"
+ res_y="${res_split[0]##*x}"
+ fi
+
+ if [[ ! "$res_x" =~ $cRESOLUTION_CHECK ]]; then
+ printf "error: x-resolution '$res_x' is not a number\n"
+ continue
+ fi
+
+ if [[ ! "$res_y" =~ $cRESOLUTION_CHECK ]]; then
+ printf "error: y-resolution '$res_y' is not a number\n"
+ continue
+ fi
+
+ for loc in "${use_loc[@]}"; do
+ loc="$(printf "%s" "$loc" | sed "s/RESX/${res_x}/g; s/RESY/${res_y}/g")"
+
+ mkdir -p "$loc" || {
+ printf "error: failed to create target directory '%s'\n" "$loc"
+ continue
+ }
+
+ # target = .png
+ local prefix="${res_split[1]}"
+ local suffix="${res_split[2]}"
+ target="$(printf "%s" "$loc/$cPREFIX$prefix$(basename "$source")" | sed "s/\.$cFILE_SUFFIX$//I")$suffix$cSUFFIX.png"
+ if [[ "$source" -nt "$target" ]] || [[ "$cFORCE" == true ]]; then
+ do_render_single "$source" "$res_x" "$res_y" "$target" || {
+ printf "error: failed to render '%s' at %sx%s\n" "$source" "$res_x" "$res_y"
+ continue
+ }
+ else
+ printf "nothing to be done for '%s' at %sx%s\n" "$source" "$res_x" "$res_y"
+ continue
+ fi
+ done
+ done
+ done
+
+ if [[ "$keep_config" != "keep" ]]; then
+ unset cNAME cITEMS cRESOLUTIONS cTARGETS cSUFFIX cPREFIX
+ # cFORCE is always preserved
+ fi
+}
diff --git a/qml/cover/CoverPage.qml b/qml/cover/CoverPage.qml
index f55dd7e..9cd6281 100644
--- a/qml/cover/CoverPage.qml
+++ b/qml/cover/CoverPage.qml
@@ -1,22 +1,49 @@
+/*
+ * This file is part of harbour-seriesfinale.
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * SPDX-FileCopyrightText: 2025 Mirian Margiani
+ */
+
import QtQuick 2.0
import Sailfish.Silica 1.0
CoverBackground {
id: coverPage
- Image {
- source: coverImage
- anchors.horizontalCenter: parent.horizontalCenter
- width: parent.width
- height: parent.height
- fillMode: Image.PreserveAspectCrop
- clip: true
- opacity: 0.5
+ Item {
+ anchors.fill: parent
+ opacity: 0.9
+
+ Image {
+ id: backgroundImage
+ source: app.coverImage
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: parent.width
+ height: parent.height
+ sourceSize {
+ width: parent.width
+ height: parent.height
+ }
+ fillMode: Image.PreserveAspectCrop
+ clip: true
+ }
+
+ OpacityRampEffect {
+ sourceItem: backgroundImage
+ direction: OpacityRamp.TopToBottom
+ slope: 2.0
+ offset: 0.3
+ }
}
Label {
id: label
- anchors.centerIn: parent
+ anchors {
+ bottom: parent.bottom
+ bottomMargin: Theme.paddingMedium
+ horizontalCenter: parent.horizontalCenter
+ }
+
text: 'SeriesFinale'
font.pixelSize: Theme.fontSizeLarge
color: Theme.highlightColor
diff --git a/qml/cover/seriesfinale_cover.png b/qml/cover/seriesfinale_cover.png
deleted file mode 100644
index 316de38..0000000
Binary files a/qml/cover/seriesfinale_cover.png and /dev/null differ
diff --git a/qml/harbour-seriesfinale.qml b/qml/harbour-seriesfinale.qml
index ccea258..2b7aad6 100644
--- a/qml/harbour-seriesfinale.qml
+++ b/qml/harbour-seriesfinale.qml
@@ -4,17 +4,18 @@ import "pages"
import "cover"
import io.thp.pyotherside 1.3
-import org.nemomobile.notifications 1.0
+import Nemo.Notifications 1.0
-ApplicationWindow
-{
- initialPage: Component { id: seriesPage; SeriesPage {} }
- cover: Component { id: coverPage; CoverPage {} }
+ApplicationWindow {
+ id: app
+
+ initialPage: Component { SeriesPage {} }
+ cover: Component { CoverPage {} }
allowedOrientations: Orientation.All
_defaultPageOrientations: Orientation.All
- property string coverImage: 'seriesfinale_cover.png'
+ property string coverImage: Qt.resolvedUrl("images/seriesfinale_cover.png")
property var prioListModel: [
{ name: qsTr("None"), color: "#93a1a1" },
@@ -45,6 +46,8 @@ ApplicationWindow
property bool ready: false
property string version
+ signal settingsChanged()
+
signal searchingChanged(bool searching)
signal updatingChanged(bool updating)
signal showUpdatingChanged(bool updating)
diff --git a/qml/images/harbour-seriesfinale.png b/qml/images/harbour-seriesfinale.png
new file mode 100644
index 0000000..5d8dc0d
Binary files /dev/null and b/qml/images/harbour-seriesfinale.png differ
diff --git a/qml/images/seriesfinale_cover.png b/qml/images/seriesfinale_cover.png
new file mode 100644
index 0000000..a63c801
Binary files /dev/null and b/qml/images/seriesfinale_cover.png differ
diff --git a/qml/modules/Opal/About/AboutPageBase.qml b/qml/modules/Opal/About/AboutPageBase.qml
new file mode 100644
index 0000000..af1669a
--- /dev/null
+++ b/qml/modules/Opal/About/AboutPageBase.qml
@@ -0,0 +1,143 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2020-2023 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import"../LinkHandler"as L
+import"private/functions.js"as Func
+import"private"
+Page{id:page
+property string appName:""
+property string appIcon:""
+property string appVersion:""
+property string appRelease:"1"
+property string appReleaseType:""
+property string description:""
+property var mainAttributions:[]
+property var authors:[]
+property var __effectiveMainAttribs:Func.makeStringListConcat(authors,mainAttributions,false)
+property string sourcesUrl:""
+property string translationsUrl:""
+property string homepageUrl:""
+property listchangelogItems
+property url changelogList
+property listlicenses
+property bool allowDownloadingLicenses:false
+property listattributions
+property bool autoAddOpalAttributions:true
+readonly property DonationsGroup donations:DonationsGroup{}
+property listextraSections
+property listcontributionSections
+property alias flickable:_flickable
+property alias _pageHeaderItem:_pageHeader
+property alias _iconItem:_icon
+property alias _develInfoSection:_develInfo
+property alias _licenseInfoSection:_licenseInfo
+property alias _donationsInfoSection:_donationsInfo
+readonly property Attribution _effectiveSelfAttribution:Attribution{name:appName
+entries:__effectiveMainAttribs
+licenses:page.licenses
+homepage:homepageUrl
+sources:sourcesUrl
+}
+function openOrCopyUrl(externalUrl,title){L.LinkHandler.openOrCopyUrl(externalUrl,title)
+}allowedOrientations:Orientation.All
+SilicaFlickable{id:_flickable
+contentHeight:column.height
+anchors.fill:parent
+VerticalScrollDecorator{}onContentHeightChanged:{if(_flickable.contentHeight>page.height&&_flickable.contentHeight-_pageHeader.origHeight+Theme.paddingMedium0||attributions.length>0
+text:__effectiveMainAttribs.join(", ")
+showMoreLabel:qsTranslate("Opal.About","show contributors")
+onClicked:{pageStack.animatorPush("private/ContributorsPage.qml",{"appName":appName,"sections":contributionSections,"attributions":attributions,"mainAttributions":__effectiveMainAttribs,"allowDownloadingLicenses":allowDownloadingLicenses,"autoAddOpalAttributions":autoAddOpalAttributions})
+}buttons:[InfoButton{text:qsTranslate("Opal.About","Homepage")
+onClicked:openOrCopyUrl(homepageUrl,text)
+enabled:homepageUrl!==""
+},InfoButton{text:qsTranslate("Opal.About","Changelog")
+onClicked:pageStack.animatorPush(Qt.resolvedUrl("private/ChangelogPage.qml"),{appName:appName,changelogItems:changelogItems,changelogList:changelogList})
+enabled:changelogItems.length>0||changelogList!=""
+},InfoButton{text:qsTranslate("Opal.About","Translations")
+onClicked:openOrCopyUrl(translationsUrl,text)
+enabled:translationsUrl!==""
+},InfoButton{text:qsTranslate("Opal.About","Source Code")
+onClicked:openOrCopyUrl(sourcesUrl,text)
+enabled:sourcesUrl!==""
+}]}Column{width:parent.width
+spacing:parent.spacing
+children:extraSections
+}InfoSection{id:_donationsInfo
+visible:donations.services.length>0||donations.text!==""
+width:parent.width
+title:qsTranslate("Opal.About","Donations")
+enabled:false
+text:donations.text===""?donations.defaultTextGeneral:donations.text
+__donationButtons:donations.services
+}InfoSection{id:_licenseInfo
+width:parent.width
+title:qsTranslate("Opal.About","License")
+enabled:licenses.length>0
+onClicked:pageStack.animatorPush("private/LicensePage.qml",{"mainAttribution":_effectiveSelfAttribution,"attributions":attributions,"allowDownloadingLicenses":allowDownloadingLicenses,"enableSourceHint":true,"includeOpal":autoAddOpalAttributions})
+text:enabled===false?"This component has been improperly configured. Please report this bug.":((licenses[0].name!==""&&licenses[0].error!==true)?licenses[0].name:licenses[0].spdxId)
+smallPrint:licenses[0].customShortText
+showMoreLabel:qsTranslate("Opal.About","show license(s)","",licenses.length+attributions.length)
+clip:true
+Behavior on height{SmoothedAnimation{duration:80
+}}}Item{id:bottomVerticalSpacing
+width:parent.width
+height:Theme.paddingMedium
+}}}Component.onCompleted:{if(__silica_applicationwindow_instance&&__silica_applicationwindow_instance.hasOwnProperty("_defaultPageOrientations")){__silica_applicationwindow_instance._defaultPageOrientations=Orientation.All
+}}}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/Attribution.qml b/qml/modules/Opal/About/Attribution.qml
new file mode 100644
index 0000000..41e9880
--- /dev/null
+++ b/qml/modules/Opal/About/Attribution.qml
@@ -0,0 +1,21 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2021-2022 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+import"private/functions.js"as Func
+QtObject{property string name
+property var entries:[]
+property listlicenses
+property string description
+property string homepage
+property string sources
+property var __effectiveEntries:Func.makeStringList(entries,false)
+property var _spdxList:null
+function _getSpdxList(force){var upd=Func.updateSpdxList(licenses,_spdxList,force)
+if(upd!==null){_spdxList=upd.spdx
+}return _spdxList
+}function _getSpdxString(append,force){var str=_getSpdxList(force).join(", ")
+if(str!==""&&append)str=str+" "+append
+return str
+}}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/ChangelogItem.qml b/qml/modules/Opal/About/ChangelogItem.qml
new file mode 100644
index 0000000..0508b40
--- /dev/null
+++ b/qml/modules/Opal/About/ChangelogItem.qml
@@ -0,0 +1,14 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2023 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+import"private/functions.js"as Func
+QtObject{property string version
+property date date:new Date(NaN)
+property string author
+property var paragraphs
+property int textFormat:Text.StyledText
+property var __effectiveEntries:Func.makeStringList(paragraphs,false)
+property string __effectiveSection:version+(isNaN(date.valueOf())?"":"|"+Qt.formatDate(date,Qt.DefaultLocaleShortDate))
+}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/ChangelogList.qml b/qml/modules/Opal/About/ChangelogList.qml
new file mode 100644
index 0000000..3e4a0b4
--- /dev/null
+++ b/qml/modules/Opal/About/ChangelogList.qml
@@ -0,0 +1,10 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2023 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+QtObject{id:root
+default property alias content:root.changelogItems
+property listchangelogItems
+readonly property int __is_opal_about_changelog_list:0
+}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/ChangelogNews.qml b/qml/modules/Opal/About/ChangelogNews.qml
new file mode 100644
index 0000000..64545bd
--- /dev/null
+++ b/qml/modules/Opal/About/ChangelogNews.qml
@@ -0,0 +1,64 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2023 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.2
+import Sailfish.Silica 1.0
+import Nemo.Configuration 1.0
+import"private/functions.js"as Func
+import"private"
+Item{id:root
+property listchangelogItems
+property url changelogList
+property string _applicationName:Qt.application.name
+property string _organizationName:Qt.application.organization
+readonly property string __lastVersion:!!configLoader.item?configLoader.item.lastVersion:""
+readonly property string __configPath:"/settings/opal/opal-about/"+"changelog-overlay/%1/%2".arg(_organizationName).arg(_applicationName)
+property list__filteredItems
+property int __ready:(configLoader.status===Loader.Ready?1:0)+(itemsLoader.effectiveItems.length>0?1:0)
+function show(){showTimer.stop()
+pageStack.completeAnimation()
+pageStack.push(dialogComponent)
+}function _markAsRead(){if(__filteredItems.length===0){return
+}var latestChangelogVersion=__filteredItems[0].version
+configLoader.item.lastVersion=latestChangelogVersion
+}Component.onCompleted:{if(!_applicationName||!_organizationName){console.warn("[Opal.About] both application name and organisation name "+"must be set in order to use the changelog overlay")
+console.warn("[Opal.About] note that these properties are also required "+"for Sailjail sandboxing")
+console.warn("[Opal.About] see: https://github.com/sailfishos/"+"sailjail-permissions#desktop-file-changes")
+}}on__ReadyChanged:{if(__ready<2||itemsLoader.effectiveItems.length===0||__filteredItems.length>0)return
+if(!!__lastVersion){var loadedItems=[]
+for(var i in itemsLoader.effectiveItems){if(itemsLoader.effectiveItems[i]===null){continue
+}var v=itemsLoader.effectiveItems[i].version
+if(v===__lastVersion){break
+}console.log("[Opal.About] showing changelog for:",v)
+loadedItems.push(itemsLoader.effectiveItems[i])
+}if(loadedItems.length>0){__filteredItems=loadedItems
+showTimer.start()
+}else{__filteredItems=itemsLoader.effectiveItems
+}}else{__filteredItems=itemsLoader.effectiveItems
+_markAsRead()
+__ready=-1
+}}Loader{id:configLoader
+sourceComponent:!!_applicationName&&!!_organizationName?configComponent:null
+asynchronous:true
+}ChangelogItemsLoader{id:itemsLoader
+changelogItems:root.changelogItems
+changelogList:root.changelogList
+}Timer{id:showTimer
+interval:10
+repeat:true
+running:false
+onTriggered:{if(pageStack.busy||pageStack.depth===0)return
+show()
+}}Component{id:configComponent
+ConfigurationGroup{path:root.__configPath
+property string lastVersion:""
+}}Component{id:dialogComponent
+Dialog{allowedOrientations:Orientation.All
+onDone:_markAsRead()
+ChangelogView{anchors.fill:parent
+changelogItems:root.__filteredItems
+header:PageHeader{title:qsTranslate("Opal.About","News")
+description:qsTranslate("Opal.About","Changes since version %1").arg(__lastVersion)
+descriptionWrapMode:Text.Wrap
+}}}}}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/ContributionGroup.qml b/qml/modules/Opal/About/ContributionGroup.qml
new file mode 100644
index 0000000..e6b5873
--- /dev/null
+++ b/qml/modules/Opal/About/ContributionGroup.qml
@@ -0,0 +1,10 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2020-2021 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+import"private/functions.js"as Func
+QtObject{property string title
+property var entries:[]
+property var __effectiveEntries:Func.makeStringList(entries)
+}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/ContributionSection.qml b/qml/modules/Opal/About/ContributionSection.qml
new file mode 100644
index 0000000..a84e24e
--- /dev/null
+++ b/qml/modules/Opal/About/ContributionSection.qml
@@ -0,0 +1,8 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2020-2021 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+QtObject{property string title
+property listgroups
+}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/DonationService.qml b/qml/modules/Opal/About/DonationService.qml
new file mode 100644
index 0000000..9dbfa84
--- /dev/null
+++ b/qml/modules/Opal/About/DonationService.qml
@@ -0,0 +1,8 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2021 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+QtObject{property string name
+property string url
+}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/InfoButton.qml b/qml/modules/Opal/About/InfoButton.qml
new file mode 100644
index 0000000..b519727
--- /dev/null
+++ b/qml/modules/Opal/About/InfoButton.qml
@@ -0,0 +1,9 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2021 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+QtObject{property string text:""
+property bool enabled:true
+signal clicked
+}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/InfoSection.qml b/qml/modules/Opal/About/InfoSection.qml
new file mode 100644
index 0000000..3876574
--- /dev/null
+++ b/qml/modules/Opal/About/InfoSection.qml
@@ -0,0 +1,106 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2020-2023 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import"../LinkHandler"as L
+Column{id:root
+spacing:0
+width:parent.width
+height:childrenRect.height
+function openOrCopyUrl(externalUrl,title){L.LinkHandler.openOrCopyUrl(externalUrl,title)
+}property alias title:_titleLabel.text
+property string text:""
+property string smallPrint:""
+property string showMoreLabel:qsTranslate("Opal.About","show details")
+property listbuttons
+property alias enabled:_bgItem.enabled
+default property alias contentItem:_contents.children
+signal clicked
+property alias _backgroundItem:_bgItem
+property alias _titleItem:_titleLabel
+property alias _textItem:_textLabel
+property alias _smallPrintItem:_smallPrintLabel
+property alias _showMoreLabelItem:_showMoreLabel
+property list__donationButtons
+BackgroundItem{id:_bgItem
+enabled:false
+width:parent.width
+height:column.height
+onClicked:root.clicked()
+Column{id:column
+width:parent.width-2*Theme.horizontalPageMargin
+height:childrenRect.height
+anchors.horizontalCenter:parent.horizontalCenter
+spacing:0
+Item{width:1
+height:Theme.paddingSmall
+}Label{id:_titleLabel
+width:parent.width
+horizontalAlignment:Text.AlignRight
+wrapMode:Text.Wrap
+font.pixelSize:Theme.fontSizeMedium
+visible:text!==""
+height:visible?implicitHeight+Theme.paddingSmall:0
+color:Theme.highlightColor
+}Item{id:_contents
+width:parent.width
+height:childrenRect.height
+}Column{width:parent.width
+spacing:Theme.paddingMedium
+visible:root.text!==""||root.smallPrint!==""
+Label{id:_textLabel
+visible:root.text!==""
+width:parent.width
+horizontalAlignment:Text.AlignLeft
+wrapMode:Text.Wrap
+text:root.text
+textFormat:Text.StyledText
+linkColor:palette.secondaryColor
+palette.primaryColor:Theme.highlightColor
+onLinkActivated:openOrCopyUrl(link)
+}Label{id:_smallPrintLabel
+visible:smallPrint!==""
+width:parent.width
+horizontalAlignment:Text.AlignLeft
+wrapMode:Text.Wrap
+text:smallPrint
+textFormat:Text.StyledText
+linkColor:palette.secondaryColor
+palette.primaryColor:Theme.highlightColor
+font.pixelSize:Theme.fontSizeSmall
+onLinkActivated:openOrCopyUrl(link)
+}Row{id:showMoreRow
+anchors.right:parent.right
+spacing:Theme.paddingSmall
+visible:root.enabled&&showMoreLabel!==""
+height:visible?_showMoreLabel.height:0
+Label{id:_showMoreLabel
+font.pixelSize:Theme.fontSizeExtraSmall
+textFormat:Text.StyledText
+text:"%1".arg(showMoreLabel)
+}Label{anchors.verticalCenter:_showMoreLabel.verticalCenter
+text:" • • •"
+}}}Item{width:1
+height:root.text!==""?Theme.paddingMedium:0
+}}}Item{width:1
+height:(buttons.length>0||__donationButtons.length>0)?Theme.paddingMedium:0
+}Column{width:parent.width
+height:childrenRect.height
+spacing:Theme.paddingMedium
+Repeater{model:buttons
+delegate:Button{anchors.horizontalCenter:parent.horizontalCenter
+width:parent.width/4*3
+height:visible?implicitHeight:0
+visible:modelData.text!==""&&modelData.enabled===true
+text:modelData.text
+onClicked:modelData.clicked()
+}}Repeater{model:__donationButtons
+delegate:Button{anchors.horizontalCenter:parent.horizontalCenter
+width:parent.width/4*3
+height:visible?implicitHeight:0
+visible:modelData.name!==""&&modelData.url!==""
+text:modelData.name
+onClicked:openOrCopyUrl(modelData.url)
+}}}}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/License.qml b/qml/modules/Opal/About/License.qml
new file mode 100644
index 0000000..cd287c9
--- /dev/null
+++ b/qml/modules/Opal/About/License.qml
@@ -0,0 +1,39 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2020-2022 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+QtObject{id:root
+property string spdxId
+property string customShortText:""
+readonly property bool error:__error
+readonly property string name:__name
+readonly property string fullText:__fullText
+property bool __online:false
+property string __localUrl:"%1/%2.json".arg(StandardPaths.temporary).arg(spdxId)
+property string __remoteUrl:"https://spdx.org/licenses/%1.json".arg(spdxId)
+property string __name:""
+property string __fullText:""
+property bool __error:false
+property bool __initialized:false
+property WorkerScript __worker:WorkerScript{source:"private/worker_spdx.js"
+onMessage:{if(messageObject.spdxId!==spdxId)return
+__name=messageObject.name
+__fullText=messageObject.fullText
+__error=messageObject.error
+if(customShortText==="")customShortText=messageObject.shortText
+}Component.onCompleted:{_load()
+__initialized=true
+}}
+onSpdxIdChanged:{if(__initialized)_load(true)
+}on__OnlineChanged:{_load()
+}function _load(force){if(fullText!==""&&force!==true)return
+if(spdxId===undefined||spdxId===""){__error=true
+console.error("[Opal.About] cannot load license without spdxId")
+return
+}__name=""
+__fullText=""
+__error=false
+__worker.sendMessage({spdxId:spdxId,localUrl:__localUrl,remoteUrl:__remoteUrl,shortText:customShortText,online:!!__online})
+}}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/OpalAboutAttribution.qml b/qml/modules/Opal/About/OpalAboutAttribution.qml
new file mode 100644
index 0000000..c9ab9d3
--- /dev/null
+++ b/qml/modules/Opal/About/OpalAboutAttribution.qml
@@ -0,0 +1,11 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2018-2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import"../../Opal/About"as A
+A.Attribution{name:"Opal.About (v3.0.0)"
+entries:["2018-2024 Mirian Margiani"]
+licenses:A.License{spdxId:"GPL-3.0-or-later"
+}sources:"https://github.com/Pretty-SFOS/opal-about"
+homepage:"https://github.com/Pretty-SFOS/opal"
+}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/private/ChangelogItemsLoader.qml b/qml/modules/Opal/About/private/ChangelogItemsLoader.qml
new file mode 100644
index 0000000..4f7c5e3
--- /dev/null
+++ b/qml/modules/Opal/About/private/ChangelogItemsLoader.qml
@@ -0,0 +1,18 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2023 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.2
+import".."
+Loader{id:root
+property listchangelogItems
+property url changelogList
+property listeffectiveItems
+asynchronous:true
+source:changelogList
+onStatusChanged:{if(status===Loader.Ready){if(!item.hasOwnProperty("__is_opal_about_changelog_list")||!item.hasOwnProperty("changelogItems")){console.error("[Opal.About] programming error: changelogList must be "+"a reference to a valid ChangelogList component")
+}else{effectiveItems=item.changelogItems
+}}}Component.onCompleted:{if(changelogItems.length>0){if(changelogList!=""){console.error("[Opal.About] programming error: it is not allowed to define "+"both changelogItems and changelogList. Changelog items in "+"the changelog list '%1' will not be shown.".arg(changelogList))
+changelogList=""
+}effectiveItems=changelogItems
+}}}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/private/ChangelogPage.qml b/qml/modules/Opal/About/private/ChangelogPage.qml
new file mode 100644
index 0000000..76d3cc7
--- /dev/null
+++ b/qml/modules/Opal/About/private/ChangelogPage.qml
@@ -0,0 +1,18 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2023 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.2
+import Sailfish.Silica 1.0
+import".."
+Page{id:root
+property string appName
+property alias changelogItems:view.changelogItems
+property alias changelogList:view.changelogList
+property alias scrollbarType:view.scrollbarType
+allowedOrientations:Orientation.All
+ChangelogView{id:view
+anchors.fill:parent
+header:PageHeader{title:qsTranslate("Opal.About","Changelog")
+description:appName
+}}}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/private/ChangelogView.qml b/qml/modules/Opal/About/private/ChangelogView.qml
new file mode 100644
index 0000000..00ceb2a
--- /dev/null
+++ b/qml/modules/Opal/About/private/ChangelogView.qml
@@ -0,0 +1,79 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2023 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.2
+import Sailfish.Silica 1.0
+import"../../LinkHandler"as L
+import".."
+import"."
+SilicaListView{id:root
+property listchangelogItems
+property url changelogList
+property int scrollbarType:ScrollbarType.auto
+property Item _scrollbar:null
+function _reloadScrollbar(){if(scrollbarType===ScrollbarType.plain){_scrollbar=null
+return
+}else if(scrollbarType===ScrollbarType.auto){var paragraphCount=0
+var useAdvanced=false
+for(var i in itemsLoader.effectiveItems){paragraphCount+=itemsLoader.effectiveItems[i].__effectiveEntries.length
+if(paragraphCount>=20){useAdvanced=true
+break
+}}}if(useAdvanced===true||scrollbarType===ScrollbarType.advanced){try{_scrollbar=Qt.createQmlObject("\n import QtQuick 2.0\n import %1 1.0 as Private\n Private.Scrollbar {\n text: root.currentSection.split('|')[0]\n description: root.currentSection.split('|').slice(1).join('|')\n headerHeight: root.headerItem ? root.headerItem.height : 0\n }".arg("Sailfish.Silica.private"),root,"Scrollbar")
+}catch(e){if(!_scrollbar){console.warn(e)
+console.warn("[Opal.About] bug: failed to load customized scrollbar")
+console.warn("[Opal.About] bug: this probably means the private API has changed")
+}}}}model:itemsLoader.effectiveItems
+spacing:Theme.paddingMedium
+quickScroll:!_scrollbar
+section.property:"__effectiveSection"
+onScrollbarTypeChanged:_reloadScrollbar()
+footer:Item{width:parent.width
+height:Theme.horizontalPageMargin
+}delegate:Column{id:item
+width:root.width
+height:childrenRect.height
+spacing:Theme.paddingSmall
+property int textFormat:model.textFormat
+property var paragraphs:model.__effectiveEntries
+Item{width:1
+height:Theme.paddingMedium
+}Label{width:parent.width-2*x
+x:Theme.horizontalPageMargin
+horizontalAlignment:Text.AlignRight
+font.pixelSize:Theme.fontSizeSmall
+truncationMode:TruncationMode.Fade
+color:palette.highlightColor
+text:model.version
+}Label{width:parent.width-2*x
+x:Theme.horizontalPageMargin
+horizontalAlignment:Text.AlignRight
+font.pixelSize:Theme.fontSizeSmall
+font.italic:true
+truncationMode:TruncationMode.Fade
+color:palette.secondaryHighlightColor
+visible:haveAuthor||haveDate
+property bool haveAuthor:!!model.author
+property bool haveDate:!isNaN(model.date.valueOf())
+text:{if(haveAuthor&&haveDate){Qt.formatDate(model.date,Qt.DefaultLocaleShortDate)+", "+model.author
+}else if(haveAuthor){model.author
+}else if(haveDate){Qt.formatDate(model.date,Qt.DefaultLocaleShortDate)
+}else{""
+}}}Repeater{model:item.paragraphs
+Label{width:parent.width-2*x
+x:Theme.horizontalPageMargin
+font.pixelSize:Theme.fontSizeSmall
+color:Theme.highlightColor
+wrapMode:Text.Wrap
+textFormat:item.textFormat
+text:modelData
+linkColor:Theme.primaryColor
+onLinkActivated:L.LinkHandler.openOrCopyUrl(link)
+bottomPadding:Theme.paddingMedium
+}}}ChangelogItemsLoader{id:itemsLoader
+changelogItems:root.changelogItems
+changelogList:root.changelogList
+onEffectiveItemsChanged:_reloadScrollbar()
+}VerticalScrollDecorator{flickable:root
+visible:!root._scrollbar||scrollbarType===ScrollbarType.none
+}}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/private/ContributorsAttributionRepeater.qml b/qml/modules/Opal/About/private/ContributorsAttributionRepeater.qml
new file mode 100644
index 0000000..b384b56
--- /dev/null
+++ b/qml/modules/Opal/About/private/ContributorsAttributionRepeater.qml
@@ -0,0 +1,27 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.2
+import Sailfish.Silica 1.0
+import"functions.js"as Func
+Repeater{delegate:DetailList{property string spdxString:modelData._getSpdxString(" • • •")
+property bool showLicensePage:false
+activeLastValue:spdxString!==""||modelData.sources!==""||modelData.homepage!==""||modelData.description!==""
+label:(modelData.__effectiveEntries.length===0&&spdxString==="")?qsTranslate("Opal.About","Thank you!"):modelData.name
+values:{var vals=Func.makeStringListConcat(modelData.__effectiveEntries,spdxString,false)
+if(vals.length===0){vals=[modelData.name]
+}if(spdxString===""){var append=""
+if(modelData.description!==""||(modelData.sources!==""&&modelData.homepage!=="")){append=qsTranslate("Opal.About","Details")
+if(modelData.description!==""){showLicensePage=true
+}}else if(modelData.sources!==""){append=qsTranslate("Opal.About","Source Code")
+}else if(modelData.homepage!==""){append=qsTranslate("Opal.About","Homepage")
+}if(append!==""){vals.push(append+" • • •")
+}}else{showLicensePage=true
+}return vals
+}onClicked:{if(showLicensePage){pageStack.animatorPush("LicensePage.qml",{"mainAttribution":modelData,"attributions":[],"allowDownloadingLicenses":allowDownloadingLicenses,"enableSourceHint":true})
+}else{var urls=[]
+if(modelData.homepage!==""){urls.push({externalUrl:modelData.homepage,title:qsTranslate("Opal.About","Homepage")})
+}if(modelData.sources!==""){urls.push({externalUrl:modelData.sources,title:qsTranslate("Opal.About","Source Code")})
+}L.LinkHandler.openOrCopyMultipleUrls(urls)
+}}}}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/private/ContributorsPage.qml b/qml/modules/Opal/About/private/ContributorsPage.qml
new file mode 100644
index 0000000..b3d224d
--- /dev/null
+++ b/qml/modules/Opal/About/private/ContributorsPage.qml
@@ -0,0 +1,43 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2020-2022 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.2
+import Sailfish.Silica 1.0
+import".."
+Page{id:root
+property listsections
+property listattributions
+property var mainAttributions:[]
+property string appName
+property bool allowDownloadingLicenses:false
+property bool autoAddOpalAttributions:false
+allowedOrientations:Orientation.All
+OpalAttributionsLoader{id:opalAttributions
+enabled:autoAddOpalAttributions
+}SilicaFlickable{anchors.fill:parent
+contentHeight:column.height+2*Theme.paddingLarge
+VerticalScrollDecorator{}Column{id:column
+width:parent.width
+spacing:Theme.paddingMedium
+PageHeader{title:qsTranslate("Opal.About","Contributors")
+}SectionHeader{text:qsTranslate("Opal.About","Development")
+visible:mainAttributions.length>0
+}DetailList{visible:mainAttributions.length>0
+label:appName
+values:mainAttributions
+}Repeater{model:sections
+delegate:Column{width:parent.width
+spacing:column.spacing
+SectionHeader{text:modelData.title
+visible:modelData.title!==""&&modelData.groups.length>0&&!(index===0&&(modelData.title==="Development"||modelData.title===qsTranslate("Opal.About","Development")))
+}Repeater{model:modelData.groups
+delegate:DetailList{label:modelData.title
+values:modelData.__effectiveEntries
+}}}}Column{width:parent.width
+spacing:column.spacing
+SectionHeader{text:qsTranslate("Opal.About","Acknowledgements")
+visible:attributions.length>0||opalAttributions.loadedAttributions.length>0
+}ContributorsAttributionRepeater{model:attributions
+}ContributorsAttributionRepeater{model:opalAttributions.loadedAttributions
+}}}}}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/private/DetailList.qml b/qml/modules/Opal/About/private/DetailList.qml
new file mode 100644
index 0000000..54bd7fe
--- /dev/null
+++ b/qml/modules/Opal/About/private/DetailList.qml
@@ -0,0 +1,22 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2020-2021 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.2
+import Sailfish.Silica 1.0
+BackgroundItem{id:root
+property string label
+property var values
+property bool activeLastValue:false
+enabled:activeLastValue
+width:parent.width
+height:column.height
+Column{id:column
+width:parent.width
+spacing:0
+Repeater{model:values.length
+delegate:DetailItem{label:index===0?root.label:""
+value:root.values[index]
+palette{secondaryHighlightColor:Theme.secondaryHighlightColor
+highlightColor:(index===values.length-1&&activeLastValue)?(root.highlighted?Theme.secondaryHighlightColor:Theme.secondaryColor):Theme.highlightColor
+}}}}}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/private/DonationsGroup.qml b/qml/modules/Opal/About/private/DonationsGroup.qml
new file mode 100644
index 0000000..71848ed
--- /dev/null
+++ b/qml/modules/Opal/About/private/DonationsGroup.qml
@@ -0,0 +1,12 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2021 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+import".."
+QtObject{readonly property string defaultTextCoffee:qsTranslate("Opal.About.Common","If you want to support my work, you can buy me a cup of coffee.")
+readonly property string defaultTextGeneral:qsTranslate("Opal.About.Common","You can support this project by contributing, or by donating using any of these services.")
+readonly property string defaultTextContribInstead:qsTranslate("Opal.About.Common","Your contributions to translations or code would be most welcome.")
+property string text:""
+property listservices
+}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/private/LicenseListPart.qml b/qml/modules/Opal/About/private/LicenseListPart.qml
new file mode 100644
index 0000000..7ec8113
--- /dev/null
+++ b/qml/modules/Opal/About/private/LicenseListPart.qml
@@ -0,0 +1,121 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2021-2023 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import"../../LinkHandler"as L
+import".."
+Column{property string title
+property bool headerVisible:title!==""
+property listlicenses
+property var extraTexts:[]
+property bool initiallyExpanded:false
+property string description:""
+property string homepage:""
+property string sources:""
+visible:licenses.length>0||description!=""
+width:parent.width
+height:childrenRect.height
+spacing:Theme.paddingSmall
+SectionHeader{visible:headerVisible
+text:title
+}Label{x:Theme.horizontalPageMargin
+visible:description!==""
+width:parent.width-2*x
+wrapMode:Text.Wrap
+text:description
+font.pixelSize:Theme.fontSizeSmall
+color:Theme.highlightColor
+bottomPadding:Theme.paddingSmall
+textFormat:Text.StyledText
+onLinkActivated:L.LinkHandler.openOrCopyUrl(link)
+linkColor:palette.secondaryHighlightColor
+palette.primaryColor:Theme.highlightColor
+}Label{x:Theme.horizontalPageMargin
+visible:text!==""
+width:parent.width-2*x
+wrapMode:Text.Wrap
+text:extraTexts.join(", ")
+font.pixelSize:Theme.fontSizeSmall
+color:Theme.highlightColor
+linkColor:Theme.primaryColor
+onLinkActivated:L.LinkHandler.openOrCopyUrl(link)
+bottomPadding:Theme.paddingSmall
+}ButtonLayout{visible:homepage!==""||sources!==""
+Button{visible:homepage!==""
+text:qsTranslate("Opal.About","Homepage")
+onClicked:L.LinkHandler.openOrCopyUrl(homepage,text)
+}Button{visible:sources!==""
+text:qsTranslate("Opal.About","Source Code")
+onClicked:L.LinkHandler.openOrCopyUrl(sources,text)
+}}Repeater{model:licenses
+delegate:Column{id:licenseColumn
+width:parent.width
+spacing:Theme.paddingSmall
+property bool expanded:initiallyExpanded
+Behavior on height{SmoothedAnimation{duration:150
+}}BackgroundItem{height:Math.max(titleColumn.height,moreIcon.height+2*Theme.paddingSmall)
+width:parent.width
+onClicked:licenseColumn.expanded=!licenseColumn.expanded
+Row{width:parent.width-Theme.horizontalPageMargin-Theme.paddingMedium
+x:Theme.horizontalPageMargin
+height:parent.height
+spacing:Theme.paddingSmall
+Column{id:titleColumn
+width:parent.width-moreIcon.width-parent.spacing
+spacing:Theme.paddingSmall
+anchors.verticalCenter:parent.verticalCenter
+Label{text:modelData.name!==""?modelData.name:modelData.spdxId
+topPadding:subtitle.visible?Theme.paddingMedium:0
+height:implicitHeight
+width:parent.width
+horizontalAlignment:Text.AlignRight
+font.pixelSize:Theme.fontSizeExtraSmall
+wrapMode:Text.Wrap
+}Label{id:subtitle
+text:modelData.spdxId
+visible:modelData.name!==""
+width:parent.width
+horizontalAlignment:Text.AlignRight
+font.pixelSize:Theme.fontSizeExtraSmall
+wrapMode:Text.Wrap
+palette.primaryColor:Theme.secondaryColor
+bottomPadding:Theme.paddingMedium
+}}HighlightImage{id:moreIcon
+anchors.verticalCenter:parent.verticalCenter
+source:"image://theme/icon-m-right"
+transformOrigin:Item.Center
+rotation:licenseColumn.expanded?90:0
+Behavior on rotation{SmoothedAnimation{duration:25
+}}}}}Item{id:licenseTextContainer
+height:licenseColumn.expanded?textLoader.height:0
+width:parent.width-2*Theme.horizontalPageMargin
+anchors.horizontalCenter:parent.horizontalCenter
+opacity:height>0?1.0:0.0
+Behavior on opacity{FadeAnimation{duration:150
+}}clip:true
+Loader{id:textLoader
+asynchronous:true
+sourceComponent:Component{Column{width:licenseTextContainer.width
+spacing:Theme.paddingMedium
+Label{visible:modelData.customShortText!==""
+text:modelData.customShortText+" ―"
+width:parent.width
+wrapMode:Text.Wrap
+font.pixelSize:Theme.fontSizeSmall
+textFormat:Text.StyledText
+palette.primaryColor:Theme.highlightColor
+linkColor:Theme.primaryColor
+onLinkActivated:L.LinkHandler.openOrCopyUrl(link)
+}Label{id:licenseTextLabel
+property bool error:modelData.error===true||modelData.fullText===""
+width:parent.width
+wrapMode:Text.Wrap
+font.pixelSize:Theme.fontSizeExtraSmall
+textFormat:error?Text.StyledText:Text.PlainText
+palette.primaryColor:Theme.highlightColor
+linkColor:Theme.primaryColor
+onLinkActivated:L.LinkHandler.openOrCopyUrl(link,error?qsTr("License text"):"")
+text:error?qsTranslate("Opal.About","Please refer to %1").arg("https://spdx.org/licenses/%1.html".arg(modelData.spdxId)):modelData.fullText
+}}}}}}}}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/private/LicenseListRepeater.qml b/qml/modules/Opal/About/private/LicenseListRepeater.qml
new file mode 100644
index 0000000..a237a68
--- /dev/null
+++ b/qml/modules/Opal/About/private/LicenseListRepeater.qml
@@ -0,0 +1,18 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+Repeater{id:root
+property bool initiallyExpanded
+property string mainModule
+delegate:LicenseListPart{title:modelData.name
+headerVisible:title!==""&&title!==mainModule
+licenses:modelData.licenses
+extraTexts:modelData.__effectiveEntries
+description:modelData.description
+initiallyExpanded:root.initiallyExpanded
+homepage:modelData.homepage
+sources:modelData.sources
+}}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/private/LicensePage.qml b/qml/modules/Opal/About/private/LicensePage.qml
new file mode 100644
index 0000000..c25b3b7
--- /dev/null
+++ b/qml/modules/Opal/About/private/LicensePage.qml
@@ -0,0 +1,59 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2020-2022 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import".."
+Page{id:root
+property Attribution mainAttribution
+property listattributions
+property bool enableSourceHint:true
+property alias pageDescription:pageHeader.description
+property bool allowDownloadingLicenses:false
+property listlicenses
+property string appName
+property string mainSources
+property string mainHomepage
+property bool includeOpal:false
+function _downloadLicenses(){for(var lic in mainAttribution.licenses){mainAttribution.licenses[lic].__online=true
+}for(var attr in attributions){for(var lic in attributions[attr].licenses){attributions[attr].licenses[lic].__online=true
+}}}allowedOrientations:Orientation.All
+OpalAttributionsLoader{id:opalAttributions
+enabled:includeOpal
+}SilicaFlickable{anchors.fill:parent
+contentHeight:column.height+Theme.horizontalPageMargin
+VerticalScrollDecorator{}PullDownMenu{visible:allowDownloadingLicenses
+enabled:visible
+MenuItem{text:qsTranslate("Opal.About","Download license texts")
+onClicked:_downloadLicenses()
+}}Column{id:column
+width:parent.width
+spacing:Theme.paddingMedium
+PageHeader{id:pageHeader
+title:(!includeOpal&&root.mainAttribution.licenses.length+attributions.length===0)?qsTranslate("Opal.About","Details"):qsTranslate("Opal.About","License(s)","",root.mainAttribution.licenses.length+attributions.length)
+description:mainAttribution.name
+}Label{visible:enableSourceHint
+width:parent.width-2*Theme.horizontalPageMargin
+height:visible?implicitHeight+Theme.paddingLarge:0
+anchors.horizontalCenter:parent.horizontalCenter
+horizontalAlignment:Text.AlignLeft
+wrapMode:Text.Wrap
+font.pixelSize:Theme.fontSizeExtraSmall
+color:Theme.highlightColor
+text:qsTranslate("Opal.About","Note: please check the source code for most accurate information.")
+}LicenseListPart{visible:root.mainAttribution.licenses.length>0||root.mainAttribution.__effectiveEntries.length>0||root.mainAttribution.description!==""
+title:root.mainAttribution.name
+headerVisible:root.mainAttribution.name!==""&&root.attributions.length>0
+licenses:root.mainAttribution.licenses
+extraTexts:root.mainAttribution.__effectiveEntries
+description:root.mainAttribution.description
+initiallyExpanded:root.mainAttribution.licenses.length===1&&root.attributions.length===0
+homepage:root.mainAttribution.homepage
+sources:root.mainAttribution.sources
+}LicenseListRepeater{model:attributions
+mainModule:root.pageDescription
+initiallyExpanded:root.licenses.length===0&&root.attributions.length===1&&root.attributions[0].licenses.length===1&&!root.includeOpal
+}LicenseListRepeater{model:opalAttributions.loadedAttributions
+initiallyExpanded:false
+}}}}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/private/OpalAttributionsLoader.qml b/qml/modules/Opal/About/private/OpalAttributionsLoader.qml
new file mode 100644
index 0000000..814d789
--- /dev/null
+++ b/qml/modules/Opal/About/private/OpalAttributionsLoader.qml
@@ -0,0 +1,60 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.2
+import Qt.labs.folderlistmodel 2.1
+FolderListModel{id:root
+property var loadedAttributions:([])
+property bool enabled:false
+property int _expectedCount:0
+property int _objectsCreated:0
+property int _round:0
+folder:enabled?Qt.resolvedUrl("../../Attributions"):""
+rootFolder:folder
+showDirs:false
+showFiles:true
+showHidden:false
+showOnlyReadable:true
+sortField:FolderListModel.Unsorted
+nameFilters:["Opal*Attribution.qml"]
+onCountChanged:{console.log("[Opal.About] loading",root.count,"Opal attributions | round",_round)
+var count=root.count
+loadedAttributions=[]
+_expectedCount=count
+_objectsCreated=0
+_round+=1
+var round=_round
+var i=0
+while(i=_expectedCount){loadedAttributions.sort(function(a,b){return a.name.localeCompare(b.name)
+})
+loadedAttributions=loadedAttributions
+}})
+i+=1
+}}function createObjectAsync(url,name,properties,parent,callback){var comp=Qt.createComponent(Qt.resolvedUrl(url),Component.Asynchronous,root)
+function _finishComponent(){if(comp.status===Component.Error){console.log("[Opal] Failed to create component “%1”:".arg(name),comp.errorString)
+callback(null,url,name)
+return true
+}else if(comp.status===Component.Ready){var incubator=comp.incubateObject(parent,properties,Qt.Asynchronous)
+function _finishObject(){if(incubator.status===Component.Error){console.log("[Opal] Failed to create object “%1”".arg(name))
+console.log(incubator.errorString)
+callback(null,url,name)
+return true
+}else if(incubator.status===Component.Ready){callback(incubator.object,url,name)
+return true
+}return false
+}
+if(!_finishObject()){incubator.onStatusChanged=function(status){_finishObject()
+}
+}return true
+}return false
+}
+if(!_finishComponent()){comp.statusChanged.connect(_finishComponent)
+}}}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/private/ScrollbarType.qml b/qml/modules/Opal/About/private/ScrollbarType.qml
new file mode 100644
index 0000000..77b3ecb
--- /dev/null
+++ b/qml/modules/Opal/About/private/ScrollbarType.qml
@@ -0,0 +1,11 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2023 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+pragma Singleton
+import QtQuick 2.0
+QtObject{readonly property int none:0
+readonly property int plain:1
+readonly property int advanced:2
+readonly property int auto:3
+}
\ No newline at end of file
diff --git a/qml/modules/Opal/About/private/functions.js b/qml/modules/Opal/About/private/functions.js
new file mode 100644
index 0000000..83e99c4
--- /dev/null
+++ b/qml/modules/Opal/About/private/functions.js
@@ -0,0 +1,6 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2021-2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+.pragma library
+function updateSpdxList(licenses,spdxTarget,force){if(spdxTarget!==null&&force!==true){return null;}var spdx=[];for(var i in licenses){spdx.push(licenses[i].spdxId);}return{spdx:spdx};};function makeStringListConcat(first,second,allowEmpty){var a=makeStringList(first,allowEmpty);var b=makeStringList(second,allowEmpty);return a.concat(b);};function makeStringList(listOrString,allowEmpty){if(!(listOrString instanceof Array))listOrString=[listOrString];var ae=(allowEmpty===true?1:0);var ret=[];for(var i in listOrString){var val=listOrString[i];var str=(typeof val==="string"?val:(!val?"":String(val))).trim();if(ae===0&&!val)continue;ret.push(val);}return ret;};function formatAppVersion(version,release,releaseType){var versionString=version;if(!!release&&release!==""&&release!=="1"){versionString+="-"+release;}if(!!releaseType&&releaseType!==""){versionString+=" (%1)".arg(releaseType);}return versionString;};
\ No newline at end of file
diff --git a/qml/modules/Opal/About/private/qmldir b/qml/modules/Opal/About/private/qmldir
new file mode 100644
index 0000000..fb828d8
--- /dev/null
+++ b/qml/modules/Opal/About/private/qmldir
@@ -0,0 +1,18 @@
+module Opal.About.private
+# This file is part of Opal.About.
+# SPDX-FileCopyrightText: 2020-2025 Mirian Margiani
+# SPDX-License-Identifier: GPL-3.0-or-later
+singleton ScrollbarType 1.0 ScrollbarType.qml
+Functions 1.0 functions.js
+WorkerSpdx 1.0 worker_spdx.js
+ChangelogItemsLoader 1.0 ChangelogItemsLoader.qml
+ChangelogPage 1.0 ChangelogPage.qml
+ChangelogView 1.0 ChangelogView.qml
+ContributorsAttributionRepeater 1.0 ContributorsAttributionRepeater.qml
+ContributorsPage 1.0 ContributorsPage.qml
+DetailList 1.0 DetailList.qml
+DonationsGroup 1.0 DonationsGroup.qml
+LicenseListPart 1.0 LicenseListPart.qml
+LicenseListRepeater 1.0 LicenseListRepeater.qml
+LicensePage 1.0 LicensePage.qml
+OpalAttributionsLoader 1.0 OpalAttributionsLoader.qml
diff --git a/qml/modules/Opal/About/private/worker_spdx.js b/qml/modules/Opal/About/private/worker_spdx.js
new file mode 100644
index 0000000..3aa2f11
--- /dev/null
+++ b/qml/modules/Opal/About/private/worker_spdx.js
@@ -0,0 +1,5 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2021-2022 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+var LOG_SCOPE="[Opal.About]";function getShortText(origShortText,spdxId){if(!!origShortText)return origShortText;if(/^[AL]?GPL-/ .test(spdxId)){return"This is free software: you are welcome to redistribute it under certain conditions. "+"There is NO WARRANTY, to the extent permitted by law.";}return origShortText;};function sendError(spdxId,shortText){WorkerScript.sendMessage({spdxId:spdxId,name:"",fullText:"",shortText:shortText,error:true});};function sendSuccess(spdxId,name,fullText,shortText){WorkerScript.sendMessage({spdxId:spdxId,name:name,fullText:fullText,shortText:shortText,error:false});};function request(type,url,onSuccess,onFailure,postData){var xhr=new XMLHttpRequest;xhr.open(type,url);xhr.onreadystatechange=function(){if(xhr.readyState===XMLHttpRequest.DONE){var response=xhr.responseText;if(response===""){onFailure(xhr);}else{onSuccess(xhr);}}};if(postData!==undefined&&type==="PUT"){xhr.send(postData);}else{xhr.send();}};function loadRemote(spdxId,localUrl,remoteUrl,origShortText){request("GET",remoteUrl,function(xhr){try{var o=JSON.parse(xhr.responseText);if(!o||typeof o!=="object")throw 1;console.log(LOG_SCOPE,"license loaded remotely from",remoteUrl);sendSuccess(spdxId,o["name"],o["licenseText"],getShortText(origShortText,spdxId));request("PUT",localUrl,function(x){console.log(LOG_SCOPE,"saved license with status",x.status,"to",localUrl);},function(x){},xhr.responseText);}catch(e){console.log(LOG_SCOPE,"failed to load license remotely from",remoteUrl);sendError(spdxId,getShortText(origShortText,spdxId));}},function(xhr){console.log(LOG_SCOPE,"failed to load license remotely from",remoteUrl);sendError(spdxId,getShortText(origShortText,spdxId));});};WorkerScript.onMessage=function(message){if(message.spdxId===undefined||message.spdxId===""){console.error(LOG_SCOPE,"cannot load license without spdx id");sendError("");return;}request("GET",message.localUrl,function(xhr){try{var o=JSON.parse(xhr.responseText);if(!o||typeof o!=="object")throw 1;console.log(LOG_SCOPE,"license loaded locally from",message.localUrl);sendSuccess(message.spdxId,o["name"],o["licenseText"],getShortText(message.shortText,message.spdxId));}catch(e){if(!!message.online){loadRemote(message.spdxId,message.localUrl,message.remoteUrl,message.shortText);}else{console.log(LOG_SCOPE,"license not cached at "+message.localUrl+", skipping download in offline mode");sendError(message.spdxId,getShortText(message.shortText,message.spdxId));}}},function(xhr){if(!!message.online){loadRemote(message.spdxId,message.localUrl,message.remoteUrl,message.shortText);}else{console.log(LOG_SCOPE,"license not cached at "+message.localUrl+", skipping download in offline mode");sendError(message.spdxId,getShortText(message.shortText,message.spdxId));}});};
\ No newline at end of file
diff --git a/qml/modules/Opal/About/qmldir b/qml/modules/Opal/About/qmldir
new file mode 100644
index 0000000..43f607d
--- /dev/null
+++ b/qml/modules/Opal/About/qmldir
@@ -0,0 +1,17 @@
+module Opal.About
+# This file is part of Opal.About.
+# SPDX-FileCopyrightText: 2020-2023 Mirian Margiani
+# SPDX-License-Identifier: GPL-3.0-or-later
+AboutPageBase 1.0 AboutPageBase.qml
+Attribution 1.0 Attribution.qml
+ContributionGroup 1.0 ContributionGroup.qml
+ContributionSection 1.0 ContributionSection.qml
+DonationService 1.0 DonationService.qml
+InfoButton 1.0 InfoButton.qml
+InfoSection 1.0 InfoSection.qml
+License 1.0 License.qml
+ChangelogItem 1.0 ChangelogItem.qml
+ChangelogList 1.0 ChangelogList.qml
+ChangelogNews 1.0 ChangelogNews.qml
+# generated files:
+OpalAboutAttribution 1.0 OpalAboutAttribution.qml
diff --git a/qml/modules/Opal/Attributions/OpalAboutAttribution.qml b/qml/modules/Opal/Attributions/OpalAboutAttribution.qml
new file mode 100644
index 0000000..c9ab9d3
--- /dev/null
+++ b/qml/modules/Opal/Attributions/OpalAboutAttribution.qml
@@ -0,0 +1,11 @@
+//@ This file is part of opal-about.
+//@ https://github.com/Pretty-SFOS/opal-about
+//@ SPDX-FileCopyrightText: 2018-2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import"../../Opal/About"as A
+A.Attribution{name:"Opal.About (v3.0.0)"
+entries:["2018-2024 Mirian Margiani"]
+licenses:A.License{spdxId:"GPL-3.0-or-later"
+}sources:"https://github.com/Pretty-SFOS/opal-about"
+homepage:"https://github.com/Pretty-SFOS/opal"
+}
\ No newline at end of file
diff --git a/qml/modules/Opal/Attributions/OpalDelegatesAttribution.qml b/qml/modules/Opal/Attributions/OpalDelegatesAttribution.qml
new file mode 100644
index 0000000..ccc883b
--- /dev/null
+++ b/qml/modules/Opal/Attributions/OpalDelegatesAttribution.qml
@@ -0,0 +1,11 @@
+//@ This file is part of opal-delegates.
+//@ https://github.com/Pretty-SFOS/opal-delegates
+//@ SPDX-FileCopyrightText: 2024-2025 Mirian Margiani (ichthyosaurus):2023 Peter G. (nephros)
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import"../../Opal/About"as A
+A.Attribution{name:"Opal.Delegates (v3.5.1)"
+entries:["2024-2025 Mirian Margiani (ichthyosaurus)","2023 Peter G. (nephros)"]
+licenses:A.License{spdxId:"GPL-3.0-or-later"
+}sources:"https://github.com/Pretty-SFOS/opal-delegates"
+homepage:"https://github.com/Pretty-SFOS/opal"
+}
\ No newline at end of file
diff --git a/qml/modules/Opal/Attributions/OpalLinkHandlerAttribution.qml b/qml/modules/Opal/Attributions/OpalLinkHandlerAttribution.qml
new file mode 100644
index 0000000..959cce9
--- /dev/null
+++ b/qml/modules/Opal/Attributions/OpalLinkHandlerAttribution.qml
@@ -0,0 +1,11 @@
+//@ This file is part of opal-linkhandler.
+//@ https://github.com/Pretty-SFOS/opal-linkhandler
+//@ SPDX-FileCopyrightText: 2020-2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import"../../Opal/About"as A
+A.Attribution{name:"Opal.LinkHandler (v2.3.0)"
+entries:["2020-2024 Mirian Margiani"]
+licenses:A.License{spdxId:"GPL-3.0-or-later"
+}sources:"https://github.com/Pretty-SFOS/opal-linkhandler"
+homepage:"https://github.com/Pretty-SFOS/opal"
+}
\ No newline at end of file
diff --git a/qml/modules/Opal/Attributions/OpalMenuSwitchAttribution.qml b/qml/modules/Opal/Attributions/OpalMenuSwitchAttribution.qml
new file mode 100644
index 0000000..419350c
--- /dev/null
+++ b/qml/modules/Opal/Attributions/OpalMenuSwitchAttribution.qml
@@ -0,0 +1,11 @@
+//@ This file is part of opal-menuswitch.
+//@ https://github.com/Pretty-SFOS/opal-menuswitch
+//@ SPDX-FileCopyrightText: 2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import"../../Opal/About"as A
+A.Attribution{name:"Opal.MenuSwitch (v1.0.1)"
+entries:["2024 Mirian Margiani"]
+licenses:A.License{spdxId:"GPL-3.0-or-later"
+}sources:"https://github.com/Pretty-SFOS/opal-menuswitch"
+homepage:"https://github.com/Pretty-SFOS/opal"
+}
\ No newline at end of file
diff --git a/qml/modules/Opal/Attributions/OpalSmartScrollbarAttribution.qml b/qml/modules/Opal/Attributions/OpalSmartScrollbarAttribution.qml
new file mode 100644
index 0000000..feb4c9b
--- /dev/null
+++ b/qml/modules/Opal/Attributions/OpalSmartScrollbarAttribution.qml
@@ -0,0 +1,11 @@
+//@ This file is part of opal-smartscrollbar.
+//@ https://github.com/Pretty-SFOS/opal-smartscrollbar
+//@ SPDX-FileCopyrightText: 2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import"../../Opal/About"as A
+A.Attribution{name:"Opal.SmartScrollbar (v1.0.0)"
+entries:["2024 Mirian Margiani"]
+licenses:A.License{spdxId:"GPL-3.0-or-later"
+}sources:"https://github.com/Pretty-SFOS/opal-smartscrollbar"
+homepage:"https://github.com/Pretty-SFOS/opal"
+}
\ No newline at end of file
diff --git a/qml/modules/Opal/Delegates/DelegateColumn.qml b/qml/modules/Opal/Delegates/DelegateColumn.qml
new file mode 100644
index 0000000..50f593d
--- /dev/null
+++ b/qml/modules/Opal/Delegates/DelegateColumn.qml
@@ -0,0 +1,12 @@
+//@ This file is part of opal-delegates.
+//@ https://github.com/Pretty-SFOS/opal-delegates
+//@ SPDX-FileCopyrightText: 2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+Column{id:root
+width:parent.width
+property alias model:repeater.model
+property alias delegate:repeater.delegate
+Repeater{id:repeater
+}}
\ No newline at end of file
diff --git a/qml/modules/Opal/Delegates/DelegateIconButton.qml b/qml/modules/Opal/Delegates/DelegateIconButton.qml
new file mode 100644
index 0000000..0d5b0d5
--- /dev/null
+++ b/qml/modules/Opal/Delegates/DelegateIconButton.qml
@@ -0,0 +1,57 @@
+//@ This file is part of opal-delegates.
+//@ https://github.com/Pretty-SFOS/opal-delegates
+//@ SPDX-FileCopyrightText: 2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+SilicaItem{id:root
+property url iconSource
+property alias iconSize:button.width
+property alias text:label.text
+property alias icon:button.icon
+property alias iconButton:button
+property alias textLabel:label
+property Item _delegate:!!parent&&parent._delegate?parent._delegate:(__padded_delegate||null)
+signal clicked(var mouse)
+signal pressAndHold(var mouse)
+width:Math.max(label.implicitWidth,button.width)
+height:Math.max(button.height+label.effectiveHeight,(!!_delegate&&_delegate.minContentHeight?_delegate.minContentHeight:0))
+highlighted:area.pressed||button.down||(!!_delegate&&_delegate.interactive&&_delegate.down)||(!!_delegate&&_delegate.menuOpen)
+enabled:!!_delegate?_delegate.enabled:true
+MouseArea{id:area
+z:-100
+anchors.fill:parent
+enabled:root.enabled
+onClicked:{root.clicked(mouse)
+}onPressAndHold:{root.clicked(mouse)
+}}SilicaItem{id:body
+width:parent.width
+height:button.height+label.effectiveHeight
+anchors.verticalCenter:parent.verticalCenter
+IconButton{id:button
+width:!!iconSource.toString()?Theme.iconSizeMedium:0
+height:width
+anchors.horizontalCenter:parent.horizontalCenter
+icon.fillMode:Image.PreserveAspectFit
+icon.source:iconSource
+enabled:root.enabled
+onClicked:{root.clicked(mouse)
+}onPressAndHold:{root.clicked(mouse)
+}Binding on highlighted{when:area.pressed||root.highlighted
+value:true
+}}OptionalLabel{id:label
+property int effectiveHeight:0
+width:parent.width
+font.pixelSize:Theme.fontSizeExtraSmall
+fontSizeMode:Text.HorizontalFit
+minimumPixelSize:0.8*Theme.fontSizeTiny
+wrapped:true
+highlighted:root.highlighted
+horizontalAlignment:Text.AlignHCenter
+anchors{top:button.bottom
+topMargin:!!text?Theme.paddingSmall:0
+horizontalCenter:parent.horizontalCenter
+}onLineLaidOut:{if(line.isLast&&!!text){effectiveHeight=line.y+line.height+anchors.topMargin
+}}Binding on effectiveHeight{when:text==""
+value:0
+}}}}
\ No newline at end of file
diff --git a/qml/modules/Opal/Delegates/DelegateIconItem.qml b/qml/modules/Opal/Delegates/DelegateIconItem.qml
new file mode 100644
index 0000000..cb4b289
--- /dev/null
+++ b/qml/modules/Opal/Delegates/DelegateIconItem.qml
@@ -0,0 +1,12 @@
+//@ This file is part of opal-delegates.
+//@ https://github.com/Pretty-SFOS/opal-delegates
+//@ SPDX-FileCopyrightText: 2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+HighlightImage{width:Theme.iconSizeMedium
+height:width
+fillMode:Image.PreserveAspectFit
+color:Theme.primaryColor
+highlighted:parent.highlighted
+}
\ No newline at end of file
diff --git a/qml/modules/Opal/Delegates/DelegateInfoItem.qml b/qml/modules/Opal/Delegates/DelegateInfoItem.qml
new file mode 100644
index 0000000..18ab2cc
--- /dev/null
+++ b/qml/modules/Opal/Delegates/DelegateInfoItem.qml
@@ -0,0 +1,121 @@
+//@ This file is part of opal-delegates.
+//@ https://github.com/Pretty-SFOS/opal-delegates
+//@ SPDX-FileCopyrightText: 2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.5
+import Sailfish.Silica 1.0
+Item{id:root
+width:Math.max(column.width,minWidth)
+height:Math.max(parent.height,column.height)
+property int minWidth:Theme.itemSizeMedium
+property int fixedWidth:0
+property int alignment:Qt.AlignHCenter
+property int __textAlignment:{if(alignment==Qt.AlignHCenter)Text.AlignHCenter
+else if(alignment==Qt.AlignLeft)Text.AlignLeft
+else if(alignment==Qt.AlignRight)Text.AlignRight
+else Text.AlignHCenter
+}property string title
+property string text
+property string description
+readonly property alias titleLabel:_line0
+readonly property alias textLabel:_line1
+readonly property alias descriptionLabel:_line2
+Column{id:column
+width:Math.max(_line0.width,_line1.width,_line2.width)
+height:Math.max(root.parent.height,_line0.height+_line1.height+_line2.height)
+anchors{horizontalCenter:parent.horizontalCenter
+verticalCenter:parent.verticalCenter
+}OptionalLabel{id:_line0
+anchors.horizontalCenter:parent.horizontalCenter
+font.pixelSize:Theme.fontSizeExtraSmall
+text:root.title
+palette{primaryColor:Theme.secondaryColor
+highlightColor:Theme.secondaryHighlightColor
+}}OptionalLabel{id:_line1
+anchors.horizontalCenter:parent.horizontalCenter
+font.pixelSize:Theme.fontSizeLarge
+text:root.text
+palette{primaryColor:Theme.primaryColor
+highlightColor:Theme.highlightColor
+}}OptionalLabel{id:_line2
+anchors.horizontalCenter:parent.horizontalCenter
+font.pixelSize:Theme.fontSizeExtraSmall
+text:root.description
+palette{primaryColor:Theme.secondaryColor
+highlightColor:Theme.secondaryHighlightColor
+}}states:[State{name:"alignLeft"
+when:alignment==Qt.AlignLeft
+AnchorChanges{target:column
+anchors.horizontalCenter:undefined
+anchors.left:parent.left
+anchors.right:undefined
+}PropertyChanges{target:_line0
+horizontalAlignment:Text.AlignLeft
+}AnchorChanges{target:_line0
+anchors.horizontalCenter:undefined
+anchors.left:parent.left
+anchors.right:undefined
+}PropertyChanges{target:_line1
+horizontalAlignment:Text.AlignLeft
+}AnchorChanges{target:_line1
+anchors.horizontalCenter:undefined
+anchors.left:parent.left
+anchors.right:undefined
+}PropertyChanges{target:_line2
+horizontalAlignment:Text.AlignLeft
+}AnchorChanges{target:_line2
+anchors.horizontalCenter:undefined
+anchors.left:parent.left
+anchors.right:undefined
+}},State{name:"alignRight"
+when:alignment==Qt.AlignRight
+AnchorChanges{target:column
+anchors.horizontalCenter:undefined
+anchors.left:undefined
+anchors.right:parent.right
+}PropertyChanges{target:_line0
+horizontalAlignment:Text.AlignRight
+}AnchorChanges{target:_line0
+anchors.horizontalCenter:undefined
+anchors.left:undefined
+anchors.right:parent.right
+}PropertyChanges{target:_line1
+horizontalAlignment:Text.AlignRight
+}AnchorChanges{target:_line1
+anchors.horizontalCenter:undefined
+anchors.left:undefined
+anchors.right:parent.right
+}PropertyChanges{target:_line2
+horizontalAlignment:Text.AlignRight
+}AnchorChanges{target:_line2
+anchors.horizontalCenter:undefined
+anchors.left:undefined
+anchors.right:parent.right
+}}]}states:[State{name:"fixedWidth"
+when:fixedWidth>0
+PropertyChanges{target:root
+width:fixedWidth
+}PropertyChanges{target:column
+width:fixedWidth
+}PropertyChanges{target:_line0
+width:fixedWidth
+wrapped:false
+horizontalAlignment:_line0.contentWidth>fixedWidth?Text.AlignLeft:__textAlignment
+}AnchorChanges{target:_line0
+anchors.horizontalCenter:undefined
+anchors.left:parent.left
+}PropertyChanges{target:_line1
+width:fixedWidth
+wrapped:false
+horizontalAlignment:_line1.contentWidth>fixedWidth?Text.AlignLeft:__textAlignment
+}AnchorChanges{target:_line1
+anchors.horizontalCenter:undefined
+anchors.left:parent.left
+}PropertyChanges{target:_line2
+width:fixedWidth
+wrapped:false
+horizontalAlignment:_line2.contentWidth>fixedWidth?Text.AlignLeft:__textAlignment
+}AnchorChanges{target:_line2
+anchors.horizontalCenter:undefined
+anchors.left:parent.left
+}}]}
\ No newline at end of file
diff --git a/qml/modules/Opal/Delegates/OneLineDelegate.qml b/qml/modules/Opal/Delegates/OneLineDelegate.qml
new file mode 100644
index 0000000..26dc77f
--- /dev/null
+++ b/qml/modules/Opal/Delegates/OneLineDelegate.qml
@@ -0,0 +1,21 @@
+//@ This file is part of opal-delegates.
+//@ https://github.com/Pretty-SFOS/opal-delegates
+//@ SPDX-FileCopyrightText: 2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+PaddedDelegate{id:root
+minContentHeight:Theme.itemSizeSmall-padding.effectiveTop-padding.effectiveBottom
+centeredContainer:contentColumn
+property string text
+readonly property alias textLabel:_line1
+readonly property alias bodyColumn:contentColumn
+Column{id:contentColumn
+width:parent.width
+OptionalLabel{id:_line1
+width:parent.width
+text:root.text
+font.pixelSize:Theme.fontSizeMedium
+palette{primaryColor:Theme.primaryColor
+highlightColor:Theme.highlightColor
+}}}}
\ No newline at end of file
diff --git a/qml/modules/Opal/Delegates/OptionalLabel.qml b/qml/modules/Opal/Delegates/OptionalLabel.qml
new file mode 100644
index 0000000..b32d5ad
--- /dev/null
+++ b/qml/modules/Opal/Delegates/OptionalLabel.qml
@@ -0,0 +1,20 @@
+//@ This file is part of opal-delegates.
+//@ https://github.com/Pretty-SFOS/opal-delegates
+//@ SPDX-FileCopyrightText: 2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.5
+import Sailfish.Silica 1.0
+Label{id:root
+property bool wrapped:false
+Binding on height{when:text==""
+value:0
+}height:implicitHeight
+wrapMode:Text.NoWrap
+truncationMode:TruncationMode.Fade
+states:[State{name:"wrapped"
+when:root.wrapped||text.indexOf("\n")>-1
+PropertyChanges{target:root
+wrapMode:Text.Wrap
+elide:Text.ElideNone
+truncationMode:TruncationMode.None
+}}]}
\ No newline at end of file
diff --git a/qml/modules/Opal/Delegates/PaddedDelegate.qml b/qml/modules/Opal/Delegates/PaddedDelegate.qml
new file mode 100644
index 0000000..8378fbc
--- /dev/null
+++ b/qml/modules/Opal/Delegates/PaddedDelegate.qml
@@ -0,0 +1,159 @@
+//@ This file is part of opal-delegates.
+//@ https://github.com/Pretty-SFOS/opal-delegates
+//@ SPDX-FileCopyrightText: 2023 Peter G. (nephros)
+//@ SPDX-FileCopyrightText: 2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import"private"
+ListItem{id:root
+property bool showOddEven:false
+property color oddColor:"transparent"
+property color evenColor:Theme.rgba(Theme.highlightBackgroundColor,Theme.opacityLow)
+property alias emphasisBackground:emphasisBackground
+property bool _isOddRow:typeof index!=="undefined"&&(index%2!=0)
+readonly property int _modelIndex:typeof index!=="undefined"?index:-1
+property bool interactive:true
+property Component leftItem:null
+readonly property alias leftItemLoader:leftItemLoader
+readonly property alias centerItem:centerItem
+property Component rightItem:null
+readonly property alias rightItemLoader:rightItemLoader
+property bool loadSideItemsAsync:false
+default property alias contents:centerItem.data
+property var centeredContainer
+property int minContentHeight:Theme.itemSizeMedium
+property int spacing:Theme.paddingMedium
+property int rightItemAlignment:Qt.AlignVCenter
+property int leftItemAlignment:Qt.AlignVCenter
+readonly property PaddingData padding:PaddingData{readonly property int __defaultLeftRight:Theme.horizontalPageMargin
+readonly property int __defaultTopBottom:Theme.paddingSmall
+leftRight:all===_undefinedValue&&(left===_undefinedValue||right===_undefinedValue)?__defaultLeftRight:NaN
+topBottom:all===_undefinedValue&&(top===_undefinedValue||bottom===_undefinedValue)?__defaultTopBottom:NaN
+}
+property Item dragHandler:null
+readonly property Item _effectiveDragHandler:!!dragHandler&&dragHandler.hasOwnProperty("__opal_view_drag_handler")?dragHandler:null
+property int dragHandleAlignment:leftItemAlignment===Qt.AlignTop||rightItemAlignment===Qt.AlignTop?Qt.AlignTop:Qt.AlignVCenter
+property bool enableDefaultGrabHandle:true
+property bool hideRightItemWhileDragging:enableDefaultGrabHandle
+readonly property bool draggable:!!_effectiveDragHandler&&!!_effectiveDragHandler.active
+function toggleWrappedText(label){label.wrapped=!label.wrapped
+}opacity:enabled?1.0:Theme.opacityLow
+Binding on highlighted{when:!interactive
+value:true
+}Binding on _backgroundColor{when:!interactive
+value:"transparent"
+}contentHeight:hidden?0:Math.max(topPaddingItem.height+bottomPaddingItem.height+Math.max(leftItemLoader.height,rightItemLoader.height,centerItem.height),minContentHeight)
+Item{id:topPaddingItem
+anchors.bottom:centerItem.top
+width:root.width
+height:padding.effectiveTop
+}Item{id:bottomPaddingItem
+anchors.top:centerItem.bottom
+width:root.width
+height:padding.effectiveBottom
+}Item{id:leftPaddingItem
+anchors.left:parent.left
+width:padding.effectiveLeft
+height:contentHeight
+}Item{id:rightPaddingItem
+anchors.right:parent.right
+width:padding.effectiveRight
+height:contentHeight
+}Loader{id:leftItemLoader
+sourceComponent:leftItem
+asynchronous:loadSideItemsAsync
+anchors{left:leftPaddingItem.right
+verticalCenter:parent.verticalCenter
+}property Item __padded_delegate:root
+Binding{target:!!leftItemLoader.item&&leftItemLoader.item.hasOwnProperty("_delegate")?leftItemLoader.item:null
+property:"_delegate"
+value:root
+}states:[State{when:leftItemAlignment==Qt.AlignVCenter
+AnchorChanges{target:leftItemLoader
+anchors.verticalCenter:leftItemLoader.parent.verticalCenter
+anchors.top:undefined
+anchors.bottom:undefined
+}},State{when:leftItemAlignment==Qt.AlignTop
+AnchorChanges{target:leftItemLoader
+anchors.verticalCenter:undefined
+anchors.top:topPaddingItem.bottom
+anchors.bottom:undefined
+}},State{when:leftItemAlignment==Qt.AlignBottom
+AnchorChanges{target:leftItemLoader
+anchors.verticalCenter:undefined
+anchors.top:undefined
+anchors.bottom:bottomPaddingItem.top
+}}]}Loader{id:rightItemLoader
+visible:!hideRightItemWhileDragging||!dragHandleLoader.visible
+sourceComponent:rightItem
+asynchronous:loadSideItemsAsync
+anchors{right:rightPaddingItem.left
+verticalCenter:parent.verticalCenter
+}property Item __padded_delegate:root
+Binding{target:!!rightItemLoader.item&&rightItemLoader.item.hasOwnProperty("_delegate")?rightItemLoader.item:null
+property:"_delegate"
+value:root
+}states:[State{when:rightItemAlignment==Qt.AlignVCenter
+AnchorChanges{target:rightItemLoader
+anchors.verticalCenter:rightItemLoader.parent.verticalCenter
+anchors.top:undefined
+anchors.bottom:undefined
+}},State{when:rightItemAlignment==Qt.AlignTop
+AnchorChanges{target:rightItemLoader
+anchors.verticalCenter:undefined
+anchors.top:topPaddingItem.bottom
+anchors.bottom:undefined
+}},State{when:rightItemAlignment==Qt.AlignBottom
+AnchorChanges{target:rightItemLoader
+anchors.verticalCenter:undefined
+anchors.top:undefined
+anchors.bottom:bottomPaddingItem.top
+}}]}Loader{id:dragHandleLoader
+visible:enableDefaultGrabHandle&&status===Loader.Ready&&draggable
+property QtObject viewHandler:_effectiveDragHandler
+property Item handledItem:root
+property int modelIndex:root._modelIndex
+source:!!_effectiveDragHandler&&enableDefaultGrabHandle?Qt.resolvedUrl("private/OptionalDragHandle.qml"):""
+asynchronous:false
+height:contentHeight
+anchors{right:rightItemLoader.left
+rightMargin:rightItemLoader.width>0?root.spacing:0
+top:parent.top
+}Binding{target:!!dragHandleLoader.item&&dragHandleLoader.item.hasOwnProperty("_delegate")?dragHandleLoader.item:null
+property:"_delegate"
+value:root
+}states:[State{when:!rightItemLoader.visible
+AnchorChanges{target:dragHandleLoader
+anchors.right:rightPaddingItem.left
+}PropertyChanges{target:dragHandleLoader
+anchors.rightMargin:0
+}}]}SilicaItem{id:centerItem
+height:Math.max(minContentHeight,childrenRect.height)
+anchors{left:leftItemLoader.right
+leftMargin:leftItemLoader.width>0?spacing:0
+right:rightItemLoader.left
+rightMargin:rightItemLoader.width>0?spacing:0
+verticalCenter:parent.verticalCenter
+}states:State{when:dragHandleLoader.visible
+AnchorChanges{target:centerItem
+anchors.right:dragHandleLoader.left
+}PropertyChanges{target:centerItem
+anchors.rightMargin:dragHandleLoader.width>0?spacing:0
+}}}Rectangle{id:emphasisBackground
+anchors.fill:parent
+visible:showOddEven
+radius:0
+opacity:Theme.opacityFaint
+color:_isOddRow?oddColor:evenColor
+}states:[State{name:"tall"
+when:!!centeredContainer&&(centeredContainer.height>minContentHeight||centeredContainer.implicitHeight>minContentHeight||centeredContainer.childrenRect.height>minContentHeight)
+AnchorChanges{target:centeredContainer
+anchors{verticalCenter:undefined
+top:centeredContainer.parent.top
+}}},State{name:"short"
+when:!!centeredContainer&&(centeredContainer.height<=minContentHeight||centeredContainer.implicitHeight<=minContentHeight||centeredContainer.childrenRect.height<=minContentHeight)
+AnchorChanges{target:centeredContainer
+anchors{top:undefined
+verticalCenter:centeredContainer.parent.verticalCenter
+}}}]}
\ No newline at end of file
diff --git a/qml/modules/Opal/Delegates/ThreeLineDelegate.qml b/qml/modules/Opal/Delegates/ThreeLineDelegate.qml
new file mode 100644
index 0000000..2acf249
--- /dev/null
+++ b/qml/modules/Opal/Delegates/ThreeLineDelegate.qml
@@ -0,0 +1,38 @@
+//@ This file is part of opal-delegates.
+//@ https://github.com/Pretty-SFOS/opal-delegates
+//@ SPDX-FileCopyrightText: 2023 Peter G. (nephros)
+//@ SPDX-FileCopyrightText: 2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+PaddedDelegate{id:root
+minContentHeight:Theme.itemSizeLarge-padding.effectiveTop-padding.effectiveBottom
+centeredContainer:contentColumn
+property string title
+property string text
+property string description
+readonly property alias titleLabel:_line0
+readonly property alias textLabel:_line1
+readonly property alias descriptionLabel:_line2
+readonly property alias bodyColumn:contentColumn
+Column{id:contentColumn
+width:parent.width
+OptionalLabel{id:_line0
+width:parent.width
+text:root.title
+font.pixelSize:Theme.fontSizeSmall
+palette{primaryColor:Theme.secondaryHighlightColor
+highlightColor:Theme.highlightColor
+}}OptionalLabel{id:_line1
+width:parent.width
+text:root.text
+font.pixelSize:Theme.fontSizeMedium
+palette{primaryColor:Theme.primaryColor
+highlightColor:Theme.highlightColor
+}}OptionalLabel{id:_line2
+width:parent.width
+text:root.description
+font.pixelSize:Theme.fontSizeSmall
+palette{primaryColor:Theme.secondaryColor
+highlightColor:Theme.secondaryHighlightColor
+}}}}
\ No newline at end of file
diff --git a/qml/modules/Opal/Delegates/TwoLineDelegate.qml b/qml/modules/Opal/Delegates/TwoLineDelegate.qml
new file mode 100644
index 0000000..ce6b989
--- /dev/null
+++ b/qml/modules/Opal/Delegates/TwoLineDelegate.qml
@@ -0,0 +1,30 @@
+//@ This file is part of opal-delegates.
+//@ https://github.com/Pretty-SFOS/opal-delegates
+//@ SPDX-FileCopyrightText: 2023 Peter G. (nephros)
+//@ SPDX-FileCopyrightText: 2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+PaddedDelegate{id:root
+minContentHeight:Theme.itemSizeMedium-padding.effectiveTop-padding.effectiveBottom
+centeredContainer:contentColumn
+property string text
+property string description
+readonly property alias textLabel:_line1
+readonly property alias descriptionLabel:_line2
+readonly property alias bodyColumn:contentColumn
+Column{id:contentColumn
+width:parent.width
+OptionalLabel{id:_line1
+width:parent.width
+text:root.text
+font.pixelSize:Theme.fontSizeMedium
+palette{primaryColor:Theme.primaryColor
+highlightColor:Theme.highlightColor
+}}OptionalLabel{id:_line2
+width:parent.width
+text:root.description
+font.pixelSize:Theme.fontSizeSmall
+palette{primaryColor:Theme.secondaryColor
+highlightColor:Theme.secondaryHighlightColor
+}}}}
\ No newline at end of file
diff --git a/qml/modules/Opal/Delegates/private/OptionalDragHandle.qml b/qml/modules/Opal/Delegates/private/OptionalDragHandle.qml
new file mode 100644
index 0000000..c3680ec
--- /dev/null
+++ b/qml/modules/Opal/Delegates/private/OptionalDragHandle.qml
@@ -0,0 +1,22 @@
+//@ This file is part of opal-delegates.
+//@ https://github.com/Pretty-SFOS/opal-delegates
+//@ SPDX-FileCopyrightText: 2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.6
+import"../../DragDrop"
+DragHandle{id:root
+property QtObject __viewHandler:viewHandler
+property Item __handledItem:handledItem
+property int __modelIndex:modelIndex
+property var _delegate
+verticalPadding:{if(!_delegate){return 0
+}else if(_delegate.dragHandleAlignment===Qt.AlignTop){return _delegate.padding.effectiveTop
+}else if(_delegate.dragHandleAlignment===Qt.AlignBottom){return _delegate.padding.effectiveBottom
+}else{return 0
+}}verticalAlignment:!!_delegate?_delegate.dragHandleAlignment:Qt.AlignVCenter
+highlighted:!!_delegate&&((_delegate.interactive&&_delegate.down)||(_delegate.menuOpen))
+moveHandler:DelegateDragHandler{id:handler
+viewHandler:__viewHandler
+handledItem:__handledItem
+modelIndex:__modelIndex
+}}
\ No newline at end of file
diff --git a/qml/modules/Opal/Delegates/private/PaddingData.qml b/qml/modules/Opal/Delegates/private/PaddingData.qml
new file mode 100644
index 0000000..7d200dd
--- /dev/null
+++ b/qml/modules/Opal/Delegates/private/PaddingData.qml
@@ -0,0 +1,21 @@
+//@ This file is part of opal-delegates.
+//@ https://github.com/Pretty-SFOS/opal-delegates
+//@ SPDX-FileCopyrightText: 2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.0
+QtObject{property int _undefinedValue:-9999
+property int all:_undefinedValue
+property int leftRight:_undefinedValue
+property int topBottom:_undefinedValue
+property int top:_undefinedValue
+property int bottom:_undefinedValue
+property int left:_undefinedValue
+property int right:_undefinedValue
+readonly property int effectiveTop:top!==_undefinedValue?top:_topBottom
+readonly property int effectiveBottom:bottom!==_undefinedValue?bottom:_topBottom
+readonly property int effectiveLeft:left!==_undefinedValue?left:_leftRight
+readonly property int effectiveRight:right!==_undefinedValue?right:_leftRight
+readonly property int _all:all!==_undefinedValue?all:0
+readonly property int _topBottom:topBottom!==_undefinedValue?topBottom:_all
+readonly property int _leftRight:leftRight!==_undefinedValue?leftRight:_all
+}
\ No newline at end of file
diff --git a/qml/modules/Opal/Delegates/qmldir b/qml/modules/Opal/Delegates/qmldir
new file mode 100644
index 0000000..0ca6ce3
--- /dev/null
+++ b/qml/modules/Opal/Delegates/qmldir
@@ -0,0 +1,14 @@
+module Opal.Delegates
+# This file is part of Opal.Delegates.
+# SPDX-FileCopyrightText: 2023 Peter G. (nephros)
+# SPDX-FileCopyrightText: 2024 Mirian Margiani
+# SPDX-License-Identifier: GPL-3.0-or-later
+PaddedDelegate 1.0 PaddedDelegate.qml
+OptionalLabel 1.0 OptionalLabel.qml
+DelegateColumn 1.0 DelegateColumn.qml
+OneLineDelegate 1.0 OneLineDelegate.qml
+TwoLineDelegate 1.0 TwoLineDelegate.qml
+ThreeLineDelegate 1.0 ThreeLineDelegate.qml
+DelegateIconItem 1.0 DelegateIconItem.qml
+DelegateInfoItem 1.0 DelegateInfoItem.qml
+DelegateIconButton 1.0 DelegateIconButton.qml
diff --git a/qml/modules/Opal/LinkHandler/LinkHandler.js b/qml/modules/Opal/LinkHandler/LinkHandler.js
new file mode 100644
index 0000000..b9b9862
--- /dev/null
+++ b/qml/modules/Opal/LinkHandler/LinkHandler.js
@@ -0,0 +1,5 @@
+//@ This file is part of opal-linkhandler.
+//@ https://github.com/Pretty-SFOS/opal-linkhandler
+//@ SPDX-FileCopyrightText: 2020-2023 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+function openOrCopyUrl(externalUrl,title){pageStack.push(Qt.resolvedUrl("private/ExternalUrlPage.qml"),{"externalUrl":externalUrl,"title":!!title?title:""});};function openOrCopyMultipleUrls(sets){var pages=[];for(var i=0;iScreen.Medium)?Theme.buttonWidthLarge:Theme.buttonWidthSmall
+Button{text:qsTranslate("Opal.LinkHandler","Copy text to clipboard")
+visible:title
+onClicked:{Clipboard.text=title
+copyNotification.publish()
+pageStack.pop()
+}}Button{ButtonLayout.newLine:root.isPortrait||Screen.sizeCategory>Screen.Medium
+text:qsTranslate("Opal.LinkHandler","Copy to clipboard")
+onClicked:{Clipboard.text=externalUrl
+copyNotification.publish()
+pageStack.pop()
+}}}ButtonLayout{preferredWidth:firstRow.preferredWidth
+Button{text:qsTranslate("Opal.LinkHandler","Share")
+onClicked:{shareHandler.resources=[{"type":"text/x-url","linkTitle":title,"status":externalUrl.toString()}]
+shareHandler.trigger()
+pageStack.pop()
+}}Button{ButtonLayout.newLine:root.isPortrait||Screen.sizeCategory>Screen.Medium
+text:/^http[s]?:\/\// .test(externalUrl)?qsTranslate("Opal.LinkHandler","Open in browser"):qsTranslate("Opal.LinkHandler","Open externally")
+onClicked:{Qt.openUrlExternally(externalUrl)
+pageStack.pop()
+}}}}}
\ No newline at end of file
diff --git a/qml/modules/Opal/LinkHandler/private/qmldir b/qml/modules/Opal/LinkHandler/private/qmldir
new file mode 100644
index 0000000..84be712
--- /dev/null
+++ b/qml/modules/Opal/LinkHandler/private/qmldir
@@ -0,0 +1,5 @@
+module Opal.LinkHandler.private
+# This file is part of Opal.LinkHandler.
+# SPDX-FileCopyrightText: 2023 Mirian Margiani
+# SPDX-License-Identifier: GPL-3.0-or-later
+ExternalUrlPage 1.0 ExternalUrlPage.qml
diff --git a/qml/modules/Opal/LinkHandler/qmldir b/qml/modules/Opal/LinkHandler/qmldir
new file mode 100644
index 0000000..e1b28af
--- /dev/null
+++ b/qml/modules/Opal/LinkHandler/qmldir
@@ -0,0 +1,5 @@
+module Opal.LinkHandler
+# This file is part of Opal.LinkHandler.
+# SPDX-FileCopyrightText: 2023 Mirian Margiani
+# SPDX-License-Identifier: GPL-3.0-or-later
+LinkHandler 1.0 LinkHandler.js
diff --git a/qml/modules/Opal/MenuSwitch/MenuSwitch.qml b/qml/modules/Opal/MenuSwitch/MenuSwitch.qml
new file mode 100644
index 0000000..6b7d042
--- /dev/null
+++ b/qml/modules/Opal/MenuSwitch/MenuSwitch.qml
@@ -0,0 +1,32 @@
+//@ This file is part of opal-menuswitch.
+//@ https://github.com/Pretty-SFOS/opal-menuswitch
+//@ SPDX-FileCopyrightText: 2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.6
+import Sailfish.Silica 1.0 as S
+S.MenuItem{id:root
+property alias automaticCheck:toggle.automaticCheck
+property alias checked:toggle.checked
+property alias busy:toggle.busy
+readonly property alias switchItem:toggle
+Binding on enabled{when:busy
+value:false
+}Binding on color{when:busy
+value:_enabledColor
+}S.TextSwitch{id:toggle
+checked:false
+automaticCheck:true
+text:""
+highlighted:parent.highlighted
+height:S.Theme.itemSizeSmall
+width:S.Theme.iconSizeMedium+S.Theme.paddingSmall
+anchors.verticalCenter:parent.verticalCenter
+onClicked:{if(!!mouse&&!automaticCheck){root.clicked()
+}}}TextMetrics{id:metrics
+font:root.font
+text:root.text
+}text:""
+property int __marginsWidth:(root.width-metrics.width)/2
+leftPadding:__marginsWidth>=toggle.width?0:toggle.width+S.Theme.paddingLarge-(Math.max(root.width-metrics.width,1.5*S.Theme.paddingLarge)/2)
+onClicked:{toggle.clicked(null)
+}}
\ No newline at end of file
diff --git a/qml/modules/Opal/MenuSwitch/qmldir b/qml/modules/Opal/MenuSwitch/qmldir
new file mode 100644
index 0000000..979a901
--- /dev/null
+++ b/qml/modules/Opal/MenuSwitch/qmldir
@@ -0,0 +1,5 @@
+module Opal.MenuSwitch
+# This file is part of Opal.MenuSwitch.
+# SPDX-FileCopyrightText: 2023-2024 Mirian Margiani
+# SPDX-License-Identifier: GPL-3.0-or-later
+MenuSwitch 1.0 MenuSwitch.qml
diff --git a/qml/modules/Opal/SmartScrollbar/SmartScrollbar.qml b/qml/modules/Opal/SmartScrollbar/SmartScrollbar.qml
new file mode 100644
index 0000000..4a32b8c
--- /dev/null
+++ b/qml/modules/Opal/SmartScrollbar/SmartScrollbar.qml
@@ -0,0 +1,30 @@
+//@ This file is part of opal-smartscrollbar.
+//@ https://github.com/Pretty-SFOS/opal-smartscrollbar
+//@ SPDX-FileCopyrightText: 2024 Mirian Margiani
+//@ SPDX-License-Identifier: GPL-3.0-or-later
+import QtQuick 2.6
+import Sailfish.Silica 1.0
+QtObject{id:root
+property Flickable flickable:null
+property string text
+property string description
+property bool smartWhen:true
+property bool quickScrollWhen:true
+readonly property bool usingFallback:_fallback.visible
+function reload(){try{_scrollbar=Qt.createQmlObject("\n import QtQuick 2.0\n import %1 1.0 as Private\n Private.Scrollbar {\n visible: root.smartWhen\n enabled: visible\n text: root.text\n description: root.description\n headerHeight: root._headerHeight\n }".arg("Sailfish.Silica.private"),flickable,"SmartScrollbar")
+}catch(e){if(!_scrollbar){console.warn(e)
+console.warn("[BUG] failed to load smart scrollbar")
+console.warn("[BUG] this probably means the private API has changed")
+}}}property int _headerHeight:!!flickable&&flickable.headerItem?flickable.headerItem.height:0
+property VerticalScrollDecorator _fallback:VerticalScrollDecorator{parent:root.flickable
+flickable:root.flickable
+visible:(!root._scrollbar||!root.smartWhen)&&!!flickable&&flickable.contentHeight>Screen.height
+}
+property Item _scrollbar:null
+property Binding __quickScroll:Binding{target:flickable
+property:"quickScroll"
+value:false
+when:!quickScrollWhen
+}
+Component.onCompleted:{reload()
+}}
\ No newline at end of file
diff --git a/qml/modules/Opal/SmartScrollbar/qmldir b/qml/modules/Opal/SmartScrollbar/qmldir
new file mode 100644
index 0000000..cf6cf58
--- /dev/null
+++ b/qml/modules/Opal/SmartScrollbar/qmldir
@@ -0,0 +1,5 @@
+module Opal.SmartScrollbar
+# This file is part of Opal.SmartScrollbar.
+# SPDX-FileCopyrightText: 2023-2024 Mirian Margiani
+# SPDX-License-Identifier: GPL-3.0-or-later
+SmartScrollbar 1.0 SmartScrollbar.qml
diff --git a/qml/pages/AboutPage.qml b/qml/pages/AboutPage.qml
index e3242ea..9f92fc5 100644
--- a/qml/pages/AboutPage.qml
+++ b/qml/pages/AboutPage.qml
@@ -1,95 +1,122 @@
+/*
+ * This file is part of harbour-seriesfinale.
+ * SPDX-FileCopyrightText: 2024-2025 Mirian Margiani
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
import QtQuick 2.0
import Sailfish.Silica 1.0
+import "../modules/Opal/About" 1.0
-Page {
- id: aboutPage
-
- property string license: 'SeriesFinale is free software: you can redistribute it ' +
- 'and/or modify it under the terms of the GNU General Public License as published by ' +
- 'the Free Software Foundation, either version 3 of the License, or ' +
- '(at your option) any later version.
' +
-
- 'SeriesFinale is distributed in the hope that it will be useful, ' +
- 'but WITHOUT ANY WARRANTY; without even the implied warranty of ' +
- 'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ' +
- 'GNU General Public License for more details.