From 8eb19772c58e56484db973c58c29249404fe2701 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Mon, 25 May 2026 22:32:11 +0200 Subject: [PATCH 1/2] testing more themes --- rust/frontend/src/models/settings.rs | 134 ++++++++- rust/zaparoo-core/src/config.rs | 14 + rust/zaparoo-core/src/persist.rs | 8 + rust/zaparoo-core/src/platform_paths.rs | 8 + src/ui/app/Main.qml | 4 +- src/ui/app/MainLayout.qml | 32 ++- src/ui/components/BrowseDetailPane.qml | 32 +-- src/ui/components/BrowseList.qml | 38 +-- src/ui/components/BrowseListDetailView.qml | 4 +- src/ui/components/HeaderBar.qml | 10 +- src/ui/components/PagedGrid.qml | 26 +- src/ui/components/Tile.qml | 2 +- src/ui/screens/GamesScreen.qml | 26 +- src/ui/screens/MediaListScreen.qml | 61 +++-- src/ui/screens/SettingsScreen.qml | 38 ++- src/ui/screens/SystemsScreen.qml | 54 ++-- src/ui/theme/BrowseLayouts.qml | 230 ++++++++-------- src/ui/theme/CMakeLists.txt | 5 + src/ui/theme/Theme.qml | 1 + src/ui/theme/ThemePalette.qml | 79 ++++++ src/ui/theme/browse-themes/crt.json | 303 +++++++++++++++++++++ src/ui/theme/browse-themes/default.json | 303 +++++++++++++++++++++ 22 files changed, 1157 insertions(+), 255 deletions(-) create mode 100644 src/ui/theme/ThemePalette.qml create mode 100644 src/ui/theme/browse-themes/crt.json create mode 100644 src/ui/theme/browse-themes/default.json diff --git a/rust/frontend/src/models/settings.rs b/rust/frontend/src/models/settings.rs index 2903f95..518641b 100644 --- a/rust/frontend/src/models/settings.rs +++ b/rust/frontend/src/models/settings.rs @@ -60,10 +60,11 @@ use crate::models::{with_persist_mut, with_persist_read}; use cxx_qt::{CxxQtType, Initialize}; use cxx_qt_lib::{QString, QStringList}; use std::pin::Pin; +use std::path::Path; use tracing::warn; use zaparoo_core::config::{load_config, save_settings_mirror, Config, SettingsMirror}; use zaparoo_core::persist::{self, SettingsState}; -use zaparoo_core::platform_paths::config_file_path; +use zaparoo_core::platform_paths::{config_file_path, themes_dir_path}; use zaparoo_core::runtime; /// Curated `MiSTer` resolution choices. Order is the left/right cycle @@ -88,6 +89,8 @@ const LANGUAGES: &[&str] = &[ "hi", ]; const DEFAULT_LANGUAGE: &str = "auto"; +const BUILTIN_THEMES: &[&str] = &["default", "crt"]; +const DEFAULT_THEME: &str = "default"; const BROWSE_LAYOUTS: &[&str] = &["grid", "list"]; const DEFAULT_BROWSE_LAYOUT: &str = "grid"; const BUTTON_LAYOUTS: &[&str] = &["a", "b", "c", "d"]; @@ -118,6 +121,9 @@ pub struct SettingsRust { current_resolution: QString, available_languages: QStringList, current_language: QString, + available_themes: QStringList, + current_theme: QString, + themes_directory_url: QString, available_browse_layouts: QStringList, current_browse_layout: QString, available_button_layouts: QStringList, @@ -147,6 +153,9 @@ pub mod ffi { #[qproperty(QString, current_resolution, READ, WRITE = set_resolution, NOTIFY)] #[qproperty(QStringList, available_languages, READ, CONSTANT)] #[qproperty(QString, current_language, READ, WRITE = set_language, NOTIFY)] + #[qproperty(QStringList, available_themes, READ, CONSTANT)] + #[qproperty(QString, current_theme, READ, WRITE = set_theme, NOTIFY)] + #[qproperty(QString, themes_directory_url, READ, CONSTANT)] #[qproperty(QStringList, available_browse_layouts, READ, CONSTANT)] #[qproperty(QString, current_browse_layout, READ, WRITE = set_browse_layout, NOTIFY)] #[qproperty(QStringList, available_button_layouts, READ, CONSTANT)] @@ -164,6 +173,9 @@ pub mod ffi { #[qinvokable] fn set_language(self: Pin<&mut Settings>, value: QString); + #[qinvokable] + fn set_theme(self: Pin<&mut Settings>, value: QString); + #[qinvokable] fn set_browse_layout(self: Pin<&mut Settings>, value: QString); @@ -192,7 +204,8 @@ impl Initialize for ffi::Settings { let config_path = config_file_path(); let is_mister = runtime::current().is_mister(); let config = load_config(&config_path); - let merged = merge_settings(&snapshot, &config); + let available_themes_vec = discover_themes(is_mister); + let merged = merge_settings(&snapshot, &config, &available_themes_vec); persist_if_changed(&snapshot, &merged); mirror_settings_to_config(&config_path, &merged); self.as_mut().rust_mut().is_mister = is_mister; @@ -204,6 +217,10 @@ impl Initialize for ffi::Settings { self.as_mut().rust_mut().current_resolution = QString::from(merged.resolution.as_str()); self.as_mut().rust_mut().available_languages = languages(); self.as_mut().rust_mut().current_language = QString::from(merged.language.as_str()); + self.as_mut().rust_mut().available_themes = qstring_list(&available_themes_vec); + self.as_mut().rust_mut().current_theme = QString::from(merged.theme.as_str()); + self.as_mut().rust_mut().themes_directory_url = + QString::from(path_to_directory_url(&themes_dir_path()).as_str()); self.as_mut().rust_mut().available_browse_layouts = browse_layouts(); self.as_mut().rust_mut().current_browse_layout = QString::from(merged.browse_layout.as_str()); @@ -250,6 +267,22 @@ impl ffi::Settings { self.as_mut().current_language_changed(); } + #[allow( + clippy::needless_pass_by_value, + reason = "cxx-qt qinvokable signature requires QString by value" + )] + fn set_theme(mut self: Pin<&mut Self>, value: QString) { + let available = self.available_themes.iter().map(String::from).collect::>(); + let value_str = normalize_theme(&value.to_string(), &available); + if self.current_theme.to_string() == value_str { + return; + } + let snapshot = persist_settings(|s| s.theme.clone_from(&value_str)); + mirror_settings_to_config(&config_file_path(), &snapshot.settings); + self.as_mut().rust_mut().current_theme = QString::from(value_str.as_str()); + self.as_mut().current_theme_changed(); + } + #[allow( clippy::needless_pass_by_value, reason = "cxx-qt qinvokable signature requires QString by value" @@ -349,12 +382,13 @@ fn persist_if_changed(current: &SettingsState, merged: &SettingsState) { persist::save(&snapshot); } -fn mirror_settings_to_config(config_path: &std::path::Path, settings: &SettingsState) { +fn mirror_settings_to_config(config_path: &Path, settings: &SettingsState) { if let Err(e) = save_settings_mirror( config_path, SettingsMirror { resolution: settings.resolution.as_str(), language: settings.language.as_str(), + theme: settings.theme.as_str(), browse_layout: settings.browse_layout.as_str(), button_layout: settings.button_layout.as_str(), mouse_enabled: settings.mouse_enabled, @@ -370,7 +404,7 @@ fn mirror_settings_to_config(config_path: &std::path::Path, settings: &SettingsS } } -fn merge_settings(snapshot: &SettingsState, config: &Config) -> SettingsState { +fn merge_settings(snapshot: &SettingsState, config: &Config, available_themes: &[String]) -> SettingsState { SettingsState { resolution: if config.video_explicit { format!("{}x{}", config.video_width, config.video_height) @@ -378,6 +412,14 @@ fn merge_settings(snapshot: &SettingsState, config: &Config) -> SettingsState { String::new() }, language: normalize_language(&config.language).to_string(), + theme: normalize_theme( + config + .settings + .theme + .as_deref() + .unwrap_or(snapshot.theme.as_str()), + available_themes, + ), browse_layout: normalize_browse_layout( config .settings @@ -416,6 +458,90 @@ fn merge_settings(snapshot: &SettingsState, config: &Config) -> SettingsState { } } +fn qstring_list(values: &[String]) -> QStringList { + let mut list = QStringList::default(); + for value in values { + list.append(QString::from(value.as_str())); + } + list +} + +fn discover_themes(is_mister: bool) -> Vec { + let mut themes = BUILTIN_THEMES + .iter() + .map(|value| (*value).to_string()) + .collect::>(); + if is_mister { + return themes; + } + + let dir = themes_dir_path(); + let Ok(entries) = std::fs::read_dir(&dir) else { + return themes; + }; + + let mut external = entries + .filter_map(Result::ok) + .filter_map(|entry| { + let path = entry.path(); + if path.extension().and_then(|ext| ext.to_str()) != Some("json") { + return None; + } + path.file_stem() + .and_then(|stem| stem.to_str()) + .map(str::trim) + .filter(|stem| !stem.is_empty()) + .map(ToOwned::to_owned) + }) + .filter(|theme| !themes.iter().any(|builtin| builtin == theme)) + .collect::>(); + external.sort_unstable(); + themes.extend(external); + themes +} + +fn normalize_theme(value: &str, available_themes: &[String]) -> String { + let trimmed = value.trim(); + if trimmed.is_empty() { + return DEFAULT_THEME.to_string(); + } + if available_themes.iter().any(|theme| theme == trimmed) { + return trimmed.to_string(); + } + DEFAULT_THEME.to_string() +} + +fn path_to_directory_url(path: &Path) -> String { + let mut url = path_to_file_url(path); + if !url.ends_with('/') { + url.push('/'); + } + url +} + +fn path_to_file_url(path: &Path) -> String { + let absolute = if path.is_absolute() { + path.to_path_buf() + } else { + std::env::current_dir() + .unwrap_or_else(|_| ".".into()) + .join(path) + }; + let path_text = absolute.to_string_lossy(); + let mut encoded = String::with_capacity(path_text.len() + 8); + for byte in path_text.as_bytes() { + match byte { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'/' => { + encoded.push(char::from(*byte)); + } + _ => { + let _ = std::fmt::Write::write_fmt(&mut encoded, format_args!("%{:02X}", byte)); + } + } + } + format!("file://{encoded}") +} + fn curated_resolutions() -> QStringList { let mut list = QStringList::default(); for r in MISTER_RESOLUTIONS { diff --git a/rust/zaparoo-core/src/config.rs b/rust/zaparoo-core/src/config.rs index 2091e74..2eb2d65 100644 --- a/rust/zaparoo-core/src/config.rs +++ b/rust/zaparoo-core/src/config.rs @@ -44,6 +44,7 @@ pub struct Config { #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct SettingsConfig { + pub theme: Option, pub browse_layout: Option, pub button_layout: Option, pub mouse_enabled: Option, @@ -55,6 +56,7 @@ pub struct SettingsConfig { pub struct SettingsMirror<'a> { pub resolution: &'a str, pub language: &'a str, + pub theme: &'a str, pub browse_layout: &'a str, pub button_layout: &'a str, pub mouse_enabled: bool, @@ -131,6 +133,7 @@ struct RawInput { #[derive(Deserialize, Default)] struct RawSettings { + theme: Option, browse_layout: Option, button_layout: Option, mouse_enabled: Option, @@ -201,6 +204,7 @@ pub fn load_config(path: &Path) -> Config { cfg.key_to_action = input_actions::invert(&merged); } cfg.settings = SettingsConfig { + theme: raw.settings.theme.map(|value| value.trim().to_string()), browse_layout: raw .settings .browse_layout @@ -273,6 +277,10 @@ pub fn save_settings_mirror(path: &Path, mirror: SettingsMirror<'_>) -> Result<( path.display() )); }; + settings.insert( + "theme".into(), + toml::Value::String(mirror.theme.trim().to_string()), + ); settings.insert( "browse_layout".into(), toml::Value::String(mirror.browse_layout.trim().to_string()), @@ -600,6 +608,7 @@ mod tests { SettingsMirror { resolution: "1280x720", language: "it_IT", + theme: "crt", browse_layout: "list", button_layout: "b", mouse_enabled: false, @@ -614,6 +623,7 @@ mod tests { assert_eq!(cfg.video_width, 1280); assert_eq!(cfg.video_height, 720); assert!(cfg.video_explicit); + assert_eq!(cfg.settings.theme.as_deref(), Some("crt")); assert_eq!(cfg.settings.browse_layout.as_deref(), Some("list")); assert_eq!(cfg.settings.button_layout.as_deref(), Some("b")); assert_eq!(cfg.settings.mouse_enabled, Some(false)); @@ -632,6 +642,7 @@ mod tests { SettingsMirror { resolution: "1280x720", language: "en", + theme: "default", browse_layout: "grid", button_layout: "a", mouse_enabled: true, @@ -648,6 +659,7 @@ mod tests { assert_eq!(cfg.core_endpoint, "ws://example.com/api"); assert_eq!(cfg.video_width, 1280); assert_eq!(cfg.video_height, 720); + assert_eq!(cfg.settings.theme.as_deref(), Some("default")); assert_eq!(cfg.settings.browse_layout.as_deref(), Some("grid")); assert_eq!(cfg.settings.button_layout.as_deref(), Some("a")); assert_eq!(cfg.settings.mouse_enabled, Some(true)); @@ -664,6 +676,7 @@ mod tests { SettingsMirror { resolution: "", language: "", + theme: "crt", browse_layout: "list", button_layout: "c", mouse_enabled: false, @@ -675,6 +688,7 @@ mod tests { .expect("save"); let written = std::fs::read_to_string(f.path()).expect("read"); assert!(written.contains("language = \"auto\"")); + assert!(written.contains("theme = \"crt\"")); assert!(written.contains("browse_layout = \"list\"")); assert!(written.contains("button_layout = \"c\"")); assert!(written.contains("mouse_enabled = false")); diff --git a/rust/zaparoo-core/src/persist.rs b/rust/zaparoo-core/src/persist.rs index 490716b..9e9a2fc 100644 --- a/rust/zaparoo-core/src/persist.rs +++ b/rust/zaparoo-core/src/persist.rs @@ -97,6 +97,8 @@ pub struct FavoritesState { pub struct SettingsState { pub resolution: String, pub language: String, + #[serde(default = "default_theme")] + pub theme: String, #[serde(default = "default_browse_layout")] pub browse_layout: String, #[serde(default = "default_button_layout")] @@ -116,6 +118,7 @@ impl Default for SettingsState { Self { resolution: String::new(), language: String::new(), + theme: default_theme(), browse_layout: default_browse_layout(), button_layout: default_button_layout(), mouse_enabled: default_mouse_enabled(), @@ -126,6 +129,10 @@ impl Default for SettingsState { } } +fn default_theme() -> String { + "default".into() +} + fn default_browse_layout() -> String { "grid".into() } @@ -270,6 +277,7 @@ mod tests { settings: SettingsState { resolution: "1920x1080".into(), language: "it_IT".into(), + theme: "crt".into(), browse_layout: "list".into(), button_layout: "b".into(), mouse_enabled: false, diff --git a/rust/zaparoo-core/src/platform_paths.rs b/rust/zaparoo-core/src/platform_paths.rs index 1e4aae1..e304605 100644 --- a/rust/zaparoo-core/src/platform_paths.rs +++ b/rust/zaparoo-core/src/platform_paths.rs @@ -63,6 +63,14 @@ pub fn state_file_path() -> PathBuf { } } +pub fn themes_dir_path() -> PathBuf { + std::env::current_exe() + .ok() + .and_then(|path| path.parent().map(std::path::Path::to_path_buf)) + .unwrap_or_else(|| PathBuf::from(".")) + .join("themes") +} + #[cfg(test)] mod tests { #![allow( diff --git a/src/ui/app/Main.qml b/src/ui/app/Main.qml index 9288d6d..63a8570 100644 --- a/src/ui/app/Main.qml +++ b/src/ui/app/Main.qml @@ -1292,7 +1292,9 @@ MainLayout { if (selectedId !== Browse.Settings.current_language) root.stageSettingRestart(fieldId, selectedId); return; - } else if (fieldId === "browseLayout") + } else if (fieldId === "theme") + Browse.Settings.set_theme(selectedId); + else if (fieldId === "browseLayout") Browse.Settings.set_browse_layout(selectedId); else if (fieldId === "buttonLayout") Browse.Settings.set_button_layout(selectedId); diff --git a/src/ui/app/MainLayout.qml b/src/ui/app/MainLayout.qml index 0c45d4a..e6c5c49 100644 --- a/src/ui/app/MainLayout.qml +++ b/src/ui/app/MainLayout.qml @@ -182,14 +182,26 @@ ApplicationWindow { Binding { target: Theme + property: "currentThemeId" + value: root.crtNativePath ? "crt" : Browse.Settings.current_theme + } + + Binding { + target: ThemePalette property: "crtNativePath" value: root.crtNativePath } + Binding { + target: Theme + property: "crtNativePath" + value: root.crtNativePath || Browse.Settings.current_theme === "crt" + } + Binding { target: Sizing property: "crtNativePath" - value: root.crtNativePath + value: root.crtNativePath || Browse.Settings.current_theme === "crt" } // Screen plumbing exposed for Main.qml's orchestration. Anything @@ -286,9 +298,17 @@ ApplicationWindow { readonly property string hubScreenState: (Browse.CategoriesModel.error_message ?? "") !== "" ? "error" : (Browse.CategoriesModel.count === 0 ? "empty" : "ready") readonly property string recentsScreenState: Browse.RecentsModel.loading ? "loading" : ((Browse.RecentsModel.error_message ?? "") !== "" ? "error" : (Browse.RecentsModel.count === 0 ? "empty" : "ready")) - readonly property bool _crtGridBrowseLayout: root.crtNativePath && Browse.Settings.current_browse_layout !== "list" - readonly property var _browseTileLayout: root.crtNativePath ? BrowseLayouts.crtTile : BrowseLayouts.defaultTile - readonly property var _contextMenuLayout: root.crtNativePath ? BrowseLayouts.crtTile : BrowseLayouts.defaultTile + readonly property bool _listBrowseLayout: Browse.Settings.current_browse_layout === "list" + readonly property string _activeBrowseProfileKey: { + if (root.activeScreen === root.screenSystems) + return root._listBrowseLayout ? "systemsList" : "systemsGrid"; + if (root.activeScreen === root.screenGames) + return root._listBrowseLayout ? "gamesList" : "gamesGrid"; + if (root.activeScreen === root.screenFavorites || root.activeScreen === root.screenRecents) + return root._listBrowseLayout ? "gamesList" : "gamesGrid"; + return root._listBrowseLayout ? "systemsList" : "systemsGrid"; + } + readonly property var _browseViewProfile: BrowseLayouts.currentProfile(root._activeBrowseProfileKey) readonly property string _crtGamesHeaderTitle: { const sid = Browse.GamesModel.current_system_id; if (sid === "") @@ -434,7 +454,7 @@ ApplicationWindow { anchors.right: parent.right anchors.top: parent.top anchors.topMargin: Sizing.headerTopMargin - layoutProfile: root._browseTileLayout + layoutProfile: root._browseViewProfile browseTitle: root.browseHeaderTitle browseProgressText: root.browseHeaderProgressText z: 200 @@ -575,7 +595,7 @@ ApplicationWindow { open: root.contextMenuVisible anchorRect: root.contextMenuAnchor entries: root.contextMenuEntries - bottomUnsafeHeight: root._contextMenuLayout.bottomUnsafeHeight + bottomUnsafeHeight: BrowseLayouts.numberValue(root._browseViewProfile, "footer.bottomUnsafeHeight", Sizing.pctH(8)) onAccepted: id => root.contextMenuAccepted(id) onCloseRequested: root.contextMenuCloseRequested() } diff --git a/src/ui/components/BrowseDetailPane.qml b/src/ui/components/BrowseDetailPane.qml index 82f4ef9..767e20a 100644 --- a/src/ui/components/BrowseDetailPane.qml +++ b/src/ui/components/BrowseDetailPane.qml @@ -23,18 +23,18 @@ Item { property string loadingText: qsTr("Loading…") property var layoutProfile: null - readonly property int _cardPaddingLeft: root.layoutProfile && root.layoutProfile.detailPanePaddingLeft !== undefined ? root.layoutProfile.detailPanePaddingLeft : Sizing.pctW(2) - readonly property int _cardPaddingRight: root.layoutProfile && root.layoutProfile.detailPanePaddingRight !== undefined ? root.layoutProfile.detailPanePaddingRight : Sizing.pctW(2) - readonly property int _cardPaddingTop: root.layoutProfile && root.layoutProfile.detailPanePaddingTop !== undefined ? root.layoutProfile.detailPanePaddingTop : Sizing.pctH(2) - readonly property int _cardPaddingBottom: root.layoutProfile && root.layoutProfile.detailPanePaddingBottom !== undefined ? root.layoutProfile.detailPanePaddingBottom : Sizing.pctH(2) + readonly property int _cardPaddingLeft: BrowseLayouts.numberValue(root.layoutProfile, "detail.panePaddingLeft", Sizing.pctW(2)) + readonly property int _cardPaddingRight: BrowseLayouts.numberValue(root.layoutProfile, "detail.panePaddingRight", Sizing.pctW(2)) + readonly property int _cardPaddingTop: BrowseLayouts.numberValue(root.layoutProfile, "detail.panePaddingTop", Sizing.pctH(2)) + readonly property int _cardPaddingBottom: BrowseLayouts.numberValue(root.layoutProfile, "detail.panePaddingBottom", Sizing.pctH(2)) readonly property int _carouselGutter: (canPreviousImage || canNextImage) ? Sizing.pctW(4) : 0 property int _labelColumnWidth: 0 readonly property int _tagTextSize: Sizing.fontSize(2.2) readonly property int _tagLabelGap: Sizing.pctW(1.4) readonly property var _detailRows: _parseDetailTags(detailTags) readonly property int _tagRowCount: _detailRows.length - readonly property int _tagRowHeight: root.layoutProfile && root.layoutProfile.detailTagRowHeight !== undefined ? root.layoutProfile.detailTagRowHeight : Sizing.pctH(3) - readonly property int _tagRowSpacing: root.layoutProfile && root.layoutProfile.detailTagRowSpacing !== undefined ? root.layoutProfile.detailTagRowSpacing : Sizing.pctH(0.55) + readonly property int _tagRowHeight: BrowseLayouts.numberValue(root.layoutProfile, "detail.tagRowHeight", Sizing.pctH(3)) + readonly property int _tagRowSpacing: BrowseLayouts.numberValue(root.layoutProfile, "detail.tagRowSpacing", Sizing.pctH(0.55)) readonly property int _metadataNaturalHeight: _tagRowCount <= 0 ? 0 : (_tagRowCount * _tagRowHeight) + ((_tagRowCount - 1) * _tagRowSpacing) readonly property int _compactDetailHeight: Math.min(Sizing.px(content.height * 0.38), _metadataNaturalHeight) readonly property bool _coverPending: coverKey === "icons/Loading" @@ -42,16 +42,16 @@ Item { readonly property bool _paneLoading: root.loading readonly property bool _detailVisible: !root._paneLoading && !root.detailSuppressed readonly property bool _suppressedPlaceholderCover: root.detailSuppressed && coverKey.startsWith("icons/") && root._coverSource !== "" - readonly property int _metadataYOffset: root.layoutProfile && root.layoutProfile.detailMetadataYOffset !== undefined ? root.layoutProfile.detailMetadataYOffset : 0 - readonly property int _metadataExtraHeight: root.layoutProfile && root.layoutProfile.detailMetadataExtraHeight !== undefined ? root.layoutProfile.detailMetadataExtraHeight : 0 - readonly property int _metadataLeftInset: root.layoutProfile && root.layoutProfile.detailMetadataLeftInset !== undefined ? root.layoutProfile.detailMetadataLeftInset : 0 - readonly property int _metadataRightInset: root.layoutProfile && root.layoutProfile.detailMetadataRightInset !== undefined ? root.layoutProfile.detailMetadataRightInset : 0 - readonly property int _imageXOffset: root.layoutProfile && root.layoutProfile.detailImageXOffset !== undefined ? root.layoutProfile.detailImageXOffset : 0 - readonly property int _imageLeftInset: root.layoutProfile && root.layoutProfile.detailImageLeftInset !== undefined ? root.layoutProfile.detailImageLeftInset : 0 - readonly property int _imageRightInset: root.layoutProfile && root.layoutProfile.detailImageRightInset !== undefined ? root.layoutProfile.detailImageRightInset : 0 - readonly property int _imageExtraWidth: root.layoutProfile && root.layoutProfile.detailImageExtraWidth !== undefined ? root.layoutProfile.detailImageExtraWidth : 0 - readonly property int _imageExtraHeight: root.layoutProfile && root.layoutProfile.detailImageExtraHeight !== undefined ? root.layoutProfile.detailImageExtraHeight : 0 - readonly property int _imageBottomGap: root.layoutProfile && root.layoutProfile.detailImageBottomGap !== undefined ? root.layoutProfile.detailImageBottomGap : 0 + readonly property int _metadataYOffset: BrowseLayouts.numberValue(root.layoutProfile, "detail.metadataYOffset", 0) + readonly property int _metadataExtraHeight: BrowseLayouts.numberValue(root.layoutProfile, "detail.metadataExtraHeight", 0) + readonly property int _metadataLeftInset: BrowseLayouts.numberValue(root.layoutProfile, "detail.metadataLeftInset", 0) + readonly property int _metadataRightInset: BrowseLayouts.numberValue(root.layoutProfile, "detail.metadataRightInset", 0) + readonly property int _imageXOffset: BrowseLayouts.numberValue(root.layoutProfile, "detail.imageXOffset", 0) + readonly property int _imageLeftInset: BrowseLayouts.numberValue(root.layoutProfile, "detail.imageLeftInset", 0) + readonly property int _imageRightInset: BrowseLayouts.numberValue(root.layoutProfile, "detail.imageRightInset", 0) + readonly property int _imageExtraWidth: BrowseLayouts.numberValue(root.layoutProfile, "detail.imageExtraWidth", 0) + readonly property int _imageExtraHeight: BrowseLayouts.numberValue(root.layoutProfile, "detail.imageExtraHeight", 0) + readonly property int _imageBottomGap: BrowseLayouts.numberValue(root.layoutProfile, "detail.imageBottomGap", 0) onDetailTagsChanged: root._labelColumnWidth = 0 diff --git a/src/ui/components/BrowseList.qml b/src/ui/components/BrowseList.qml index 26f7177..9f9e9b2 100644 --- a/src/ui/components/BrowseList.qml +++ b/src/ui/components/BrowseList.qml @@ -20,31 +20,33 @@ Item { property var layoutProfile: null readonly property int itemCount: listView.count readonly property int totalItems: totalItemsOverride >= 0 ? totalItemsOverride : itemCount - readonly property int _selectionRadius: root.layoutProfile ? root.layoutProfile.tileCornerRadius : Sizing.cornerRadius - readonly property int cardPaddingLeft: root.layoutProfile ? root.layoutProfile.listCardPaddingLeft : Sizing.pctW(2) - readonly property int cardPaddingRight: root.layoutProfile ? root.layoutProfile.listCardPaddingRight : Sizing.pctW(2) - readonly property int cardPaddingTop: root.layoutProfile ? root.layoutProfile.listCardPaddingTop : Sizing.pctH(2) - readonly property int cardPaddingBottom: root.layoutProfile ? root.layoutProfile.listCardPaddingBottom : Sizing.pctH(2) - readonly property int rowSpacing: root.layoutProfile ? root.layoutProfile.listRowSpacing : Sizing.pctH(0.7) + readonly property int _selectionRadius: BrowseLayouts.numberValue(root.layoutProfile, "surface.cornerRadius", Sizing.cornerRadius) + readonly property int cardPaddingLeft: BrowseLayouts.numberValue(root.layoutProfile, "list.cardPaddingLeft", Sizing.pctW(2)) + readonly property int cardPaddingRight: BrowseLayouts.numberValue(root.layoutProfile, "list.cardPaddingRight", Sizing.pctW(2)) + readonly property int cardPaddingTop: BrowseLayouts.numberValue(root.layoutProfile, "list.cardPaddingTop", Sizing.pctH(2)) + readonly property int cardPaddingBottom: BrowseLayouts.numberValue(root.layoutProfile, "list.cardPaddingBottom", Sizing.pctH(2)) + readonly property int rowSpacing: BrowseLayouts.numberValue(root.layoutProfile, "list.rowSpacing", Sizing.pctH(0.7)) readonly property int contentHeight: Math.max(0, height - cardPaddingTop - cardPaddingBottom) - readonly property int rowHeight: root.layoutProfile && root.layoutProfile.listRowHeight > 0 ? root.layoutProfile.listRowHeight : (targetVisibleRowCount > 0 ? Math.max(Sizing.pctH(3), Math.floor((contentHeight - (rowSpacing * (targetVisibleRowCount - 1))) / targetVisibleRowCount)) : Sizing.pctH(6)) + readonly property int _profileRowHeight: BrowseLayouts.numberValue(root.layoutProfile, "list.rowHeight", 0) + readonly property int rowHeight: _profileRowHeight > 0 ? _profileRowHeight : (targetVisibleRowCount > 0 ? Math.max(Sizing.pctH(3), Math.floor((contentHeight - (rowSpacing * (targetVisibleRowCount - 1))) / targetVisibleRowCount)) : Sizing.pctH(6)) readonly property int rowStride: rowHeight + rowSpacing readonly property int visibleRowCount: targetVisibleRowCount > 0 ? targetVisibleRowCount : Math.max(1, Math.floor((contentHeight + rowSpacing) / rowStride)) - readonly property int _centerSlot: root.layoutProfile && root.layoutProfile.listCenterSlot >= 0 ? Math.max(0, Math.min(visibleRowCount - 1, root.layoutProfile.listCenterSlot)) : Math.max(0, Math.floor((visibleRowCount - 1) / 2)) + readonly property int _profileCenterSlot: BrowseLayouts.numberValue(root.layoutProfile, "list.centerSlot", -1) + readonly property int _centerSlot: _profileCenterSlot >= 0 ? Math.max(0, Math.min(visibleRowCount - 1, _profileCenterSlot)) : Math.max(0, Math.floor((visibleRowCount - 1) / 2)) readonly property int _maxViewTopIndex: Math.max(0, itemCount - visibleRowCount) readonly property int _viewTopIndex: Math.max(0, Math.min(_maxViewTopIndex, currentIndex - _centerSlot)) readonly property int _targetContentY: _viewTopIndex * rowStride readonly property int _maxScrollTopIndex: Math.max(0, totalItems - visibleRowCount) - readonly property int _gutterWidth: root.layoutProfile ? root.layoutProfile.gridGutterWidth : Sizing.pctW(3) - readonly property int _gutterGap: root.layoutProfile && root.layoutProfile.listScrollbarGap !== undefined ? root.layoutProfile.listScrollbarGap : (root.layoutProfile ? root.layoutProfile.gridGutterGap : Sizing.pctW(1.5)) - readonly property int _scrollThumbWidth: root.layoutProfile ? root.layoutProfile.scrollThumbWidth : Sizing.pctW(1.2) - readonly property int _scrollThumbRightInset: root.layoutProfile ? root.layoutProfile.scrollThumbRightInset : 0 - readonly property bool _scrollThumbRightAligned: root.layoutProfile && root.layoutProfile.scrollThumbRightAligned !== undefined ? root.layoutProfile.scrollThumbRightAligned : false - readonly property int _scrollArrowSize: root.layoutProfile ? root.layoutProfile.scrollArrowSize : Math.min(root._gutterWidth, Sizing.pctH(4)) - readonly property int _selectionAccentWidth: root.layoutProfile && root.layoutProfile.listSelectionAccentWidth !== undefined ? root.layoutProfile.listSelectionAccentWidth : Sizing.pctW(0.45) - readonly property int _rowTextLeftPadding: root.layoutProfile ? root.layoutProfile.listRowTextLeftPadding : Sizing.pctW(1.6) - readonly property int _rowTextRightPadding: root.layoutProfile ? root.layoutProfile.listRowTextRightPadding : Sizing.pctW(1.6) - readonly property int _favoriteRightPadding: root.layoutProfile ? root.layoutProfile.listFavoriteRightPadding : Sizing.pctW(1.6) + readonly property int _gutterWidth: BrowseLayouts.numberValue(root.layoutProfile, "grid.gutterWidth", Sizing.pctW(3)) + readonly property int _gutterGap: BrowseLayouts.numberValue(root.layoutProfile, "list.scrollbarGap", BrowseLayouts.numberValue(root.layoutProfile, "grid.gutterGap", Sizing.pctW(1.5))) + readonly property int _scrollThumbWidth: BrowseLayouts.numberValue(root.layoutProfile, "grid.scrollThumbWidth", Sizing.pctW(1.2)) + readonly property int _scrollThumbRightInset: BrowseLayouts.numberValue(root.layoutProfile, "grid.scrollThumbRightInset", 0) + readonly property bool _scrollThumbRightAligned: BrowseLayouts.boolValue(root.layoutProfile, "grid.scrollThumbRightAligned", false) + readonly property int _scrollArrowSize: BrowseLayouts.numberValue(root.layoutProfile, "grid.scrollArrowSize", Math.min(root._gutterWidth, Sizing.pctH(4))) + readonly property int _selectionAccentWidth: BrowseLayouts.numberValue(root.layoutProfile, "list.selectionAccentWidth", Sizing.pctW(0.45)) + readonly property int _rowTextLeftPadding: BrowseLayouts.numberValue(root.layoutProfile, "list.rowTextLeftPadding", Sizing.pctW(1.6)) + readonly property int _rowTextRightPadding: BrowseLayouts.numberValue(root.layoutProfile, "list.rowTextRightPadding", Sizing.pctW(1.6)) + readonly property int _favoriteRightPadding: BrowseLayouts.numberValue(root.layoutProfile, "list.favoriteRightPadding", Sizing.pctW(1.6)) signal itemHovered(int index) signal itemClicked(int index) diff --git a/src/ui/components/BrowseListDetailView.qml b/src/ui/components/BrowseListDetailView.qml index 0dbe1f6..fcf1b35 100644 --- a/src/ui/components/BrowseListDetailView.qml +++ b/src/ui/components/BrowseListDetailView.qml @@ -30,8 +30,8 @@ Item { property alias detailSuppressed: detailPane.detailSuppressed property alias detailCanPreviousImage: detailPane.canPreviousImage property alias detailCanNextImage: detailPane.canNextImage - readonly property int _cardRadius: root.layoutProfile ? root.layoutProfile.tileCornerRadius : Sizing.cornerRadius - readonly property int _dividerOffsetX: root.layoutProfile && root.layoutProfile.listDividerOffsetX !== undefined ? root.layoutProfile.listDividerOffsetX : 0 + readonly property int _cardRadius: BrowseLayouts.numberValue(root.layoutProfile, "surface.cornerRadius", Sizing.cornerRadius) + readonly property int _dividerOffsetX: BrowseLayouts.numberValue(root.layoutProfile, "list.dividerOffsetX", 0) signal itemHovered(int index) signal itemClicked(int index) diff --git a/src/ui/components/HeaderBar.qml b/src/ui/components/HeaderBar.qml index 60db43b..472e664 100644 --- a/src/ui/components/HeaderBar.qml +++ b/src/ui/components/HeaderBar.qml @@ -80,8 +80,8 @@ Item { Row { id: topHud - anchors.top: header.layoutProfile && header.layoutProfile.headerHudBottomAligned ? undefined : parent.top - anchors.bottom: header.layoutProfile && header.layoutProfile.headerHudBottomAligned ? parent.bottom : undefined + anchors.top: BrowseLayouts.boolValue(header.layoutProfile, "header.hudBottomAligned", false) ? undefined : parent.top + anchors.bottom: BrowseLayouts.boolValue(header.layoutProfile, "header.hudBottomAligned", false) ? parent.bottom : undefined anchors.right: parent.right anchors.rightMargin: Sizing.headerSideMargin spacing: Sizing.pctW(1) @@ -161,7 +161,7 @@ Item { Text { id: crtTitleLabel - visible: header.layoutProfile && header.layoutProfile.showHeaderTitleInHeader && header.browseTitle !== "" + visible: BrowseLayouts.boolValue(header.layoutProfile, "header.titleInHeader", false) && header.browseTitle !== "" x: Sizing.center(parent.width, width) y: parent.height - height width: Math.min(Math.floor(parent.width / 3), Math.ceil(Math.max(crtTitleMetrics.advanceWidth, crtTitleMetrics.boundingRect.width))) @@ -183,8 +183,8 @@ Item { // idle, but its slot stays reserved by the header's fixed height // so the logo and the surrounding layout don't shift. CoreStatusPill { - anchors.top: header.layoutProfile && header.layoutProfile.headerStatusPillPinnedTop ? parent.top : topHud.bottom + anchors.top: BrowseLayouts.boolValue(header.layoutProfile, "header.statusPillPinnedTop", false) ? parent.top : topHud.bottom anchors.right: topHud.right - anchors.topMargin: header.layoutProfile && header.layoutProfile.headerStatusPillPinnedTop ? 0 : Sizing.headerStackGap + anchors.topMargin: BrowseLayouts.boolValue(header.layoutProfile, "header.statusPillPinnedTop", false) ? 0 : Sizing.headerStackGap } } diff --git a/src/ui/components/PagedGrid.qml b/src/ui/components/PagedGrid.qml index 96ec69f..3d347f9 100644 --- a/src/ui/components/PagedGrid.qml +++ b/src/ui/components/PagedGrid.qml @@ -164,20 +164,20 @@ Item { // not as another cell, so a full inter-cell gap looks like wasted // space next to it. The gutter stays reserved on a single page // (just hidden) so cells don't reflow when paging activates. - readonly property int leftInset: root.layoutProfile ? root.layoutProfile.gridLeftInset : Sizing.pctW(5) - readonly property int rightInset: root.layoutProfile ? root.layoutProfile.gridRightInset : Sizing.pctW(5) - readonly property int gutterWidth: root.layoutProfile ? root.layoutProfile.gridGutterWidth : Sizing.pctW(3) - readonly property int gutterGap: root.layoutProfile ? root.layoutProfile.gridGutterGap : Sizing.pctW(1.5) - readonly property int scrollThumbWidth: root.layoutProfile ? root.layoutProfile.scrollThumbWidth : Sizing.pctW(1.2) - readonly property int scrollThumbRightInset: root.layoutProfile ? root.layoutProfile.scrollThumbRightInset : 0 - readonly property bool scrollThumbRightAligned: root.layoutProfile && root.layoutProfile.scrollThumbRightAligned !== undefined ? root.layoutProfile.scrollThumbRightAligned : false - readonly property int scrollArrowSize: root.layoutProfile ? root.layoutProfile.scrollArrowSize : Math.min(gutterWidth, Sizing.pctH(4)) - readonly property int topInset: root.layoutProfile ? root.layoutProfile.gridTopInset : Sizing.pctH(2) - readonly property int bottomInset: root.layoutProfile ? root.layoutProfile.gridBottomInset : Sizing.pctH(2) - readonly property int cellSpacingX: root.layoutProfile ? root.layoutProfile.gridColumnGap : Sizing.pctW(3) - readonly property int cellSpacingY: root.layoutProfile ? root.layoutProfile.gridRowGap : Sizing.pctH(4) + readonly property int leftInset: BrowseLayouts.numberValue(root.layoutProfile, "grid.leftInset", Sizing.pctW(5)) + readonly property int rightInset: BrowseLayouts.numberValue(root.layoutProfile, "grid.rightInset", Sizing.pctW(5)) + readonly property int gutterWidth: BrowseLayouts.numberValue(root.layoutProfile, "grid.gutterWidth", Sizing.pctW(3)) + readonly property int gutterGap: BrowseLayouts.numberValue(root.layoutProfile, "grid.gutterGap", Sizing.pctW(1.5)) + readonly property int scrollThumbWidth: BrowseLayouts.numberValue(root.layoutProfile, "grid.scrollThumbWidth", Sizing.pctW(1.2)) + readonly property int scrollThumbRightInset: BrowseLayouts.numberValue(root.layoutProfile, "grid.scrollThumbRightInset", 0) + readonly property bool scrollThumbRightAligned: BrowseLayouts.boolValue(root.layoutProfile, "grid.scrollThumbRightAligned", false) + readonly property int scrollArrowSize: BrowseLayouts.numberValue(root.layoutProfile, "grid.scrollArrowSize", Math.min(gutterWidth, Sizing.pctH(4))) + readonly property int topInset: BrowseLayouts.numberValue(root.layoutProfile, "grid.topInset", Sizing.pctH(2)) + readonly property int bottomInset: BrowseLayouts.numberValue(root.layoutProfile, "grid.bottomInset", Sizing.pctH(2)) + readonly property int cellSpacingX: BrowseLayouts.numberValue(root.layoutProfile, "grid.columnGap", Sizing.pctW(3)) + readonly property int cellSpacingY: BrowseLayouts.numberValue(root.layoutProfile, "grid.rowGap", Sizing.pctH(4)) readonly property int _contentWidth: root.columns * root.cellWidth + (root.columns - 1) * root.cellSpacingX - readonly property int _scrollGutterX: root.layoutProfile && root.layoutProfile.packHorizontalRemainderAfterGutter ? root.leftInset + root._contentWidth + root.gutterGap : width - root.rightInset - root.gutterWidth + readonly property int _scrollGutterX: BrowseLayouts.boolValue(root.layoutProfile, "grid.packHorizontalRemainderAfterGutter", false) ? root.leftInset + root._contentWidth + root.gutterGap : width - root.rightInset - root.gutterWidth // Computed cell dimensions — fill the available area, divided by // gridColumns × gridRows. Callers don't override. The cell area diff --git a/src/ui/components/Tile.qml b/src/ui/components/Tile.qml index f4d8146..c904203 100644 --- a/src/ui/components/Tile.qml +++ b/src/ui/components/Tile.qml @@ -87,7 +87,7 @@ Item { readonly property int _captionHeight: Sizing.pctH(5.5) readonly property int _captionGap: Sizing.pctH(0.4) readonly property int _captionTextSize: Sizing.fontSize(2.2) - readonly property int _tileCornerRadius: root.layoutProfile ? root.layoutProfile.tileCornerRadius : Sizing.cornerRadius + readonly property int _tileCornerRadius: BrowseLayouts.numberValue(root.layoutProfile, "surface.cornerRadius", Sizing.cornerRadius) readonly property int _captionTextMaxWidth: Math.max(0, root.width - 2 * root._tileCornerRadius) readonly property int _textMeasureSlack: Theme.crtNativePath ? 0 : 2 readonly property int _captionMeasuredWidth: Math.ceil(Math.max(captionMetrics.advanceWidth, captionMetrics.boundingRect.width) + root._textMeasureSlack) diff --git a/src/ui/screens/GamesScreen.qml b/src/ui/screens/GamesScreen.qml index 88b6274..79deea6 100644 --- a/src/ui/screens/GamesScreen.qml +++ b/src/ui/screens/GamesScreen.qml @@ -3,7 +3,6 @@ // SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 import QtQuick -import Zaparoo.Theme import Zaparoo.Ui import Zaparoo.Browse as Browse @@ -26,8 +25,7 @@ MediaListScreen { readonly property int _listPageSize: 10 readonly property int _browsePageSize: games._listLayout ? Math.max(1, games.listCard.visibleRowCount) : games.gamesGrid.pageSize - readonly property bool _crtGridLayout: Theme.crtNativePath && !games._listLayout - readonly property var _tileLayout: games._crtGridLayout ? BrowseLayouts.crtTile : BrowseLayouts.defaultTile + readonly property var _gridProfile: BrowseLayouts.currentProfile("gamesGrid") mediaModel: Browse.GamesModel emptyText: qsTr("No games in this system") @@ -94,9 +92,10 @@ MediaListScreen { else games.requestSystemsScreen(); } - showTopStrip: games._tileLayout.showTopStrip + gridProfileKey: "gamesGrid" + listProfileKey: "gamesList" topStripTitleProvider: () => { - if (games._tileLayout.showHeaderTitleInHeader) + if (BrowseLayouts.boolValue(games._viewProfile, "header.titleInHeader", false)) return ""; const sid = Browse.GamesModel.current_system_id; if (sid === "") @@ -105,8 +104,8 @@ MediaListScreen { return idx >= 0 ? Browse.SystemsModel.system_name_at(idx) : sid; } topStripCurrentPageProvider: () => Math.floor(games.gamesGrid.currentIndex / games._browsePageSize) - topStripTotalPagesProvider: () => games._tileLayout.showBottomStatusRow ? 1 : Math.max(1, Math.ceil((Browse.GamesModel.dir_count + Browse.GamesModel.total_files) / games._browsePageSize)) - topStripTotalTextProvider: () => games._listLayout || games._tileLayout.showBottomStatusRow ? "" : (Browse.GamesModel.total_files > 0 ? qsTr("%1 files").arg(Browse.GamesModel.total_files) : "") + topStripTotalPagesProvider: () => BrowseLayouts.boolValue(games._gridProfile, "footer.bottomStatusVisible", false) ? 1 : Math.max(1, Math.ceil((Browse.GamesModel.dir_count + Browse.GamesModel.total_files) / games._browsePageSize)) + topStripTotalTextProvider: () => games._listLayout || BrowseLayouts.boolValue(games._gridProfile, "footer.bottomStatusVisible", false) ? "" : (Browse.GamesModel.total_files > 0 ? qsTr("%1 files").arg(Browse.GamesModel.total_files) : "") topStripRightTextProvider: () => { if (!games._listLayout) return ""; @@ -117,8 +116,6 @@ MediaListScreen { const total = Math.max(1, Browse.GamesModel.dir_count + Browse.GamesModel.total_files); return qsTr("%1 / %2").arg(games.gamesGrid.currentIndex + 1).arg(total); } - gridLayoutProfile: games._tileLayout - gridBottomMargin: games._tileLayout.showBottomStatusRow ? games._tileLayout.activeLabelBottomMargin + games._tileLayout.activeLabelHeight : Sizing.pctH(6) + games._tileLayout.activeLabelBottomMargin + games._tileLayout.activeLabelHeight gridTotalItemsOverride: Browse.GamesModel.dir_count + Browse.GamesModel.total_files gridHasMorePages: Browse.GamesModel.has_next_page gridLoadMoreAction: () => Browse.GamesModel.fetch_more() @@ -130,15 +127,10 @@ MediaListScreen { } activeLabelTextProvider: () => games.gamesGrid.itemCount > 0 ? Browse.GamesModel.name_at(games.gamesGrid.currentIndex) : "" activeLabelAtBottom: true - activeLabelBottomMargin: games._tileLayout.activeLabelBottomMargin - activeLabelHeight: games._tileLayout.activeLabelHeight - showBottomStatusRow: games._tileLayout.showBottomStatusRow - bottomStatusLeftMargin: games._tileLayout.bottomStatusLeftMargin - bottomStatusRightMargin: games._tileLayout.bottomStatusRightMargin - bottomStatusLeftText: games._tileLayout.showBottomStatusRow && Browse.GamesModel.total_files > 0 ? qsTr("%1 files").arg(Browse.GamesModel.total_files) : "" - bottomStatusRightText: games._tileLayout.showBottomStatusRow && Math.ceil((Browse.GamesModel.dir_count + Browse.GamesModel.total_files) / games._browsePageSize) > 1 ? qsTr("%1 / %2").arg(Math.floor(games.gamesGrid.currentIndex / games._browsePageSize) + 1).arg(Math.max(1, Math.ceil((Browse.GamesModel.dir_count + Browse.GamesModel.total_files) / games._browsePageSize))) : "" + bottomStatusLeftText: BrowseLayouts.boolValue(games._gridProfile, "footer.bottomStatusVisible", false) && Browse.GamesModel.total_files > 0 ? qsTr("%1 files").arg(Browse.GamesModel.total_files) : "" + bottomStatusRightText: BrowseLayouts.boolValue(games._gridProfile, "footer.bottomStatusVisible", false) && Math.ceil((Browse.GamesModel.dir_count + Browse.GamesModel.total_files) / games._browsePageSize) > 1 ? qsTr("%1 / %2").arg(Math.floor(games.gamesGrid.currentIndex / games._browsePageSize) + 1).arg(Math.max(1, Math.ceil((Browse.GamesModel.dir_count + Browse.GamesModel.total_files) / games._browsePageSize))) : "" pageLoadingVisible: !games._listLayout && Browse.GamesModel.loading_more && games.gamesGrid.hasPendingTarget - pageLoadingLeftMargin: games._tileLayout.showBottomStatusRow && games.bottomStatusLeftText !== "" ? Sizing.px(games.width / 3) : games.gamesGrid.leftInset + pageLoadingLeftMargin: BrowseLayouts.boolValue(games._gridProfile, "footer.bottomStatusVisible", false) && games.bottomStatusLeftText !== "" ? Sizing.px(games.width / 3) : games.gamesGrid.leftInset Binding { target: Browse.GamesModel diff --git a/src/ui/screens/MediaListScreen.qml b/src/ui/screens/MediaListScreen.qml index 4144855..e02e4bc 100644 --- a/src/ui/screens/MediaListScreen.qml +++ b/src/ui/screens/MediaListScreen.qml @@ -66,26 +66,31 @@ Item { property bool gridFocused: true property bool detailRapidScrollActive: false property bool forceListLayout: false + property string gridProfileKey: "gamesGrid" + property string listProfileKey: "gamesList" property bool renderGridLayout: true - property bool showTopStrip: true - property bool showBottomStatusRow: false - property bool showHeaderTitleInHeader: false property bool activeLabelAtBottom: false - property int gridBottomMargin: Sizing.pctH(15) - property int activeLabelBottomMargin: 0 - property int activeLabelHeight: Sizing.pctH(7) - property int bottomStatusLeftMargin: 0 - property int bottomStatusRightMargin: 0 + property int gridBottomMargin: 0 property int pageLoadingLeftMargin: 0 property bool pageLoadingVisible: false property string bottomStatusLeftText: "" property string bottomStatusRightText: "" - property var gridLayoutProfile: null property int gridTotalItemsOverride: -1 property bool gridHasMorePages: false readonly property bool _listLayout: root.forceListLayout || Browse.Settings.current_browse_layout === "list" - readonly property bool _crtListStrip: Theme.crtNativePath && root._listLayout - readonly property var _listLayoutProfile: Theme.crtNativePath ? BrowseLayouts.crtTile : BrowseLayouts.defaultTile + readonly property var _gridProfile: BrowseLayouts.currentProfile(root.gridProfileKey) + readonly property var _listProfile: BrowseLayouts.currentProfile(root.listProfileKey) + readonly property var _viewProfile: root._listLayout ? root._listProfile : root._gridProfile + readonly property int _activeLabelHeight: BrowseLayouts.numberValue(root._gridProfile, "footer.activeLabelHeight", Sizing.pctH(7)) + readonly property int _activeLabelBottomMargin: BrowseLayouts.numberValue(root._gridProfile, "footer.activeLabelBottomMargin", 0) + readonly property bool _bottomStatusVisible: BrowseLayouts.boolValue(root._gridProfile, "footer.bottomStatusVisible", false) + readonly property int _bottomStatusLeftMargin: BrowseLayouts.numberValue(root._gridProfile, "footer.bottomStatusLeftMargin", 0) + readonly property int _bottomStatusRightMargin: BrowseLayouts.numberValue(root._gridProfile, "footer.bottomStatusRightMargin", 0) + readonly property bool _topStripVisible: BrowseLayouts.boolValue(root._viewProfile, "status.topStripVisible", true) + readonly property int _topStripHeight: BrowseLayouts.numberValue(root._viewProfile, "status.stripHeight", Sizing.pctH(7)) + readonly property int _topStripSlotMargin: BrowseLayouts.numberValue(root._viewProfile, "status.slotMargin", Sizing.pctW(5)) + readonly property int _defaultGridBottomMargin: root._bottomStatusVisible ? root._activeLabelBottomMargin + root._activeLabelHeight : Sizing.pctH(6) + root._activeLabelBottomMargin + root._activeLabelHeight + readonly property int _effectiveGridBottomMargin: root.gridBottomMargin > 0 ? root.gridBottomMargin : root._defaultGridBottomMargin readonly property int _listOverlayBottomMargin: Sizing.pctH(15) readonly property bool _gateHide: root.transitioning || root._loading() @@ -285,13 +290,13 @@ Item { TopStatusStrip { id: topStrip - visible: !root._gateHide && (root.showTopStrip || root._crtListStrip) + visible: !root._gateHide && root._topStripVisible anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.topMargin: Sizing.headerBottom + Sizing.pctH(1) - height: root._crtListStrip ? root._listLayoutProfile.listStripHeight : (root.showTopStrip ? Sizing.pctH(7) : 0) - slotMargin: root._crtListStrip ? root._listLayoutProfile.listStripSlotMargin : Sizing.pctW(5) + height: root._topStripHeight + slotMargin: root._topStripSlotMargin title: typeof root.topStripTitleProvider === "function" ? root.topStripTitleProvider() : root.screenTitle currentPage: typeof root.topStripCurrentPageProvider === "function" ? root.topStripCurrentPageProvider() : mediaGrid.currentPage totalPages: typeof root.topStripTotalPagesProvider === "function" ? root.topStripTotalPagesProvider() : Math.max(1, Math.ceil(root._count() / mediaGrid.pageSize)) @@ -304,14 +309,14 @@ Item { visible: !root._gateHide && root._listLayout anchors.left: parent.left - anchors.leftMargin: root._listLayoutProfile.listCardSideMargin + anchors.leftMargin: BrowseLayouts.numberValue(root._listProfile, "list.cardSideMargin", Sizing.pctW(5)) anchors.right: parent.right - anchors.rightMargin: root._listLayoutProfile.listCardSideMargin + anchors.rightMargin: BrowseLayouts.numberValue(root._listProfile, "list.cardSideMargin", Sizing.pctW(5)) anchors.top: topStrip.bottom anchors.topMargin: Sizing.pctH(2) anchors.bottom: parent.bottom anchors.bottomMargin: Sizing.pctH(8) - layoutProfile: root._listLayoutProfile + layoutProfile: root._listProfile model: root.mediaModel totalItemsOverride: root.totalItemsOverride targetVisibleRowCount: root.targetVisibleRowCount @@ -348,14 +353,14 @@ Item { anchors.right: parent.right anchors.top: topStrip.bottom anchors.bottom: parent.bottom - anchors.bottomMargin: root.gridBottomMargin + anchors.bottomMargin: root._effectiveGridBottomMargin focused: root.gridFocused model: root.mediaModel delegate: Tile { - layoutProfile: root.gridLayoutProfile + layoutProfile: root._gridProfile showCaption: true } - layoutProfile: root.gridLayoutProfile + layoutProfile: root._gridProfile columnsOverride: Sizing.gamesGridColumns rowsOverride: Sizing.gamesGridRows totalItemsOverride: root.gridTotalItemsOverride @@ -395,18 +400,18 @@ Item { anchors.right: parent.right anchors.top: root.activeLabelAtBottom ? undefined : mediaGrid.bottom anchors.bottom: root.activeLabelAtBottom ? parent.bottom : undefined - anchors.bottomMargin: root.activeLabelAtBottom ? root.activeLabelBottomMargin : 0 - height: root.activeLabelHeight + anchors.bottomMargin: root.activeLabelAtBottom ? root._activeLabelBottomMargin : 0 + height: root._activeLabelHeight text: typeof root.activeLabelTextProvider === "function" ? root.activeLabelTextProvider() : (mediaGrid.itemCount > 0 ? root.mediaModel.name_at(mediaGrid.currentIndex) : "") } Text { id: bottomTotalText - visible: root.showBottomStatusRow && !root._gateHide && !root._listLayout && root.bottomStatusLeftText !== "" + visible: root._bottomStatusVisible && !root._gateHide && !root._listLayout && root.bottomStatusLeftText !== "" anchors.left: parent.left - anchors.leftMargin: root.bottomStatusLeftMargin + anchors.leftMargin: root._bottomStatusLeftMargin anchors.verticalCenter: activeLabel.verticalCenter - width: Sizing.px(parent.width / 3) - root.bottomStatusLeftMargin + width: Sizing.px(parent.width / 3) - root._bottomStatusLeftMargin height: Sizing.fontSize(2.9) elide: Text.ElideRight horizontalAlignment: Text.AlignLeft @@ -419,11 +424,11 @@ Item { } Text { - visible: root.showBottomStatusRow && !root._gateHide && !root._listLayout && root.bottomStatusRightText !== "" + visible: root._bottomStatusVisible && !root._gateHide && !root._listLayout && root.bottomStatusRightText !== "" anchors.right: parent.right - anchors.rightMargin: root.bottomStatusRightMargin + anchors.rightMargin: root._bottomStatusRightMargin anchors.verticalCenter: activeLabel.verticalCenter - width: Sizing.px(parent.width / 3) - root.bottomStatusRightMargin + width: Sizing.px(parent.width / 3) - root._bottomStatusRightMargin height: Sizing.fontSize(2.9) elide: Text.ElideRight horizontalAlignment: Text.AlignRight diff --git a/src/ui/screens/SettingsScreen.qml b/src/ui/screens/SettingsScreen.qml index 0f0d1e6..7265afe 100644 --- a/src/ui/screens/SettingsScreen.qml +++ b/src/ui/screens/SettingsScreen.qml @@ -72,6 +72,13 @@ Item { id: "language", label: qsTr("Language") }); + if (!Browse.Runtime.is_mister) { + out.push({ + kind: "field", + id: "theme", + label: qsTr("Theme") + }); + } out.push({ kind: "field", id: "browseLayout", @@ -242,7 +249,7 @@ Item { if (!settings._isField(settings.currentIndex)) return false; const id = settings.fields[settings.currentIndex].id; - return id === "language" || id === "browseLayout" || id === "buttonLayout" || id === "resolution"; + return id === "language" || id === "theme" || id === "browseLayout" || id === "buttonLayout" || id === "resolution"; } // True when the focused field is an action button (updateMediaDb, // runScraper, uploadLog, aboutLicense). Drives the help-bar Accept @@ -332,6 +339,11 @@ Item { return raw === undefined || raw === null ? [] : raw; } + function _themeList(): list { + const raw = Browse.Settings.available_themes; + return raw === undefined || raw === null ? [] : raw; + } + function _languageList(): list { const raw = Browse.Settings.available_languages; return raw === undefined || raw === null ? [] : raw; @@ -375,6 +387,19 @@ Item { return qsTr("Grid view"); } + function _themeDisplay(value: string): string { + if (value === "crt") + return qsTr("CRT"); + if (value === "default" || value === "") + return qsTr("Default"); + const words = value.replace(/[-_]+/g, " ").split(" "); + for (let i = 0; i < words.length; i++) { + if (words[i].length > 0) + words[i] = words[i][0].toUpperCase() + words[i].slice(1); + } + return words.join(" "); + } + function _buttonLayoutDisplay(value: string): string { if (value === "b") return qsTr("Style B"); @@ -437,6 +462,15 @@ Item { label: settings._languageDisplay(list[i]) }); initialId = Browse.Settings.current_language; + } else if (id === "theme") { + title = qsTr("Theme"); + const list = settings._themeList(); + for (let i = 0; i < list.length; i++) + entries.push({ + id: list[i], + label: settings._themeDisplay(list[i]) + }); + initialId = Browse.Settings.current_theme; } else if (id === "browseLayout") { title = qsTr("Browsing layout"); const list = settings._browseLayoutList(); @@ -667,7 +701,7 @@ Item { // `_triggerIndex`/`_triggerScrape`. enabled: row.modelData.id === "updateMediaDb" ? !settings._scrapeBusy : row.modelData.id === "runScraper" ? !settings._indexBusy : true label: row.modelData.label - value: row.modelData.id === "resolution" ? settings._resolutionDisplay(Browse.Settings.current_resolution) : row.modelData.id === "language" ? settings._languageDisplay(Browse.Settings.current_language) : row.modelData.id === "browseLayout" ? settings._browseLayoutDisplay(Browse.Settings.current_browse_layout) : row.modelData.id === "buttonLayout" ? settings._buttonLayoutDisplay(Browse.Settings.current_button_layout) : row.modelData.id === "screensaverTimeout" ? settings._screensaverTimeoutDisplay(Browse.Settings.current_screensaver_timeout) : "" + value: row.modelData.id === "resolution" ? settings._resolutionDisplay(Browse.Settings.current_resolution) : row.modelData.id === "language" ? settings._languageDisplay(Browse.Settings.current_language) : row.modelData.id === "theme" ? settings._themeDisplay(Browse.Settings.current_theme) : row.modelData.id === "browseLayout" ? settings._browseLayoutDisplay(Browse.Settings.current_browse_layout) : row.modelData.id === "buttonLayout" ? settings._buttonLayoutDisplay(Browse.Settings.current_button_layout) : row.modelData.id === "screensaverTimeout" ? settings._screensaverTimeoutDisplay(Browse.Settings.current_screensaver_timeout) : "" control: row.modelData.id === "mouseEnabled" || row.modelData.id === "discoverArcadeAlternateVersions" || row.modelData.id === "debugLogging" ? "toggle" : row.modelData.id === "aboutLicense" ? "navigate" : (row.modelData.id === "updateMediaDb" || row.modelData.id === "runScraper" || row.modelData.id === "uploadLog") ? "action" : "picker" checked: row.modelData.id === "debugLogging" ? Browse.Settings.current_debug_logging : row.modelData.id === "discoverArcadeAlternateVersions" ? Browse.Settings.current_discover_arcade_alternate_versions : Browse.Settings.current_mouse_enabled actionStatus: row.modelData.id === "updateMediaDb" ? settings._indexActionStatus() : row.modelData.id === "runScraper" ? settings._scrapeActionStatus() : "" diff --git a/src/ui/screens/SystemsScreen.qml b/src/ui/screens/SystemsScreen.qml index 97c45bf..120ba8e 100644 --- a/src/ui/screens/SystemsScreen.qml +++ b/src/ui/screens/SystemsScreen.qml @@ -3,7 +3,6 @@ // SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 import QtQuick -import Zaparoo.Theme import Zaparoo.Ui import Zaparoo.Browse as Browse @@ -37,10 +36,16 @@ Item { // times. The ring restores automatically when the modal pops. property bool gridFocused: true readonly property bool _listLayout: Browse.Settings.current_browse_layout === "list" - readonly property bool _crtGridLayout: Theme.crtNativePath && !systems._listLayout - readonly property bool _crtListStrip: Theme.crtNativePath && systems._listLayout - readonly property var _tileLayout: Theme.crtNativePath ? BrowseLayouts.crtTile : BrowseLayouts.defaultTile - readonly property int _listOverlayBottomMargin: Sizing.pctH(6) + systems._tileLayout.activeLabelBottomMargin + systems._tileLayout.activeLabelHeight + readonly property string _profileKey: systems._listLayout ? "systemsList" : "systemsGrid" + readonly property var _viewProfile: BrowseLayouts.currentProfile(systems._profileKey) + readonly property var _gridProfile: BrowseLayouts.currentProfile("systemsGrid") + readonly property int _activeLabelHeight: BrowseLayouts.numberValue(systems._gridProfile, "footer.activeLabelHeight", Sizing.pctH(7)) + readonly property int _activeLabelBottomMargin: BrowseLayouts.numberValue(systems._gridProfile, "footer.activeLabelBottomMargin", 0) + readonly property bool _bottomStatusVisible: BrowseLayouts.boolValue(systems._gridProfile, "footer.bottomStatusVisible", false) + readonly property int _bottomStatusLeftMargin: BrowseLayouts.numberValue(systems._gridProfile, "footer.bottomStatusLeftMargin", 0) + readonly property int _bottomStatusRightMargin: BrowseLayouts.numberValue(systems._gridProfile, "footer.bottomStatusRightMargin", 0) + readonly property int _gridBottomMargin: systems._bottomStatusVisible ? systems._activeLabelBottomMargin + systems._activeLabelHeight : Sizing.pctH(6) + systems._activeLabelBottomMargin + systems._activeLabelHeight + readonly property int _listOverlayBottomMargin: systems._gridBottomMargin signal requestAccept(systemId: string) signal requestHubScreen @@ -179,14 +184,14 @@ Item { anchors.right: parent.right anchors.top: parent.top anchors.topMargin: Sizing.headerBottom + Sizing.pctH(1) - height: systems._crtListStrip ? systems._tileLayout.listStripHeight : (systems._tileLayout.showTopStrip ? Sizing.pctH(7) : 0) - slotMargin: systems._crtListStrip ? systems._tileLayout.listStripSlotMargin : Sizing.pctW(5) - title: systems._tileLayout.showHeaderTitleInHeader ? "" : Browse.SystemsModel.current_category + height: BrowseLayouts.numberValue(systems._viewProfile, "status.stripHeight", Sizing.pctH(7)) + slotMargin: BrowseLayouts.numberValue(systems._viewProfile, "status.slotMargin", Sizing.pctW(5)) + title: BrowseLayouts.boolValue(systems._viewProfile, "header.titleInHeader", false) ? "" : Browse.SystemsModel.current_category currentPage: systemsGrid.currentPage - totalPages: systems._tileLayout.showBottomStatusRow ? 1 : Math.max(1, Math.ceil(Browse.SystemsModel.count / systemsGrid.pageSize)) - totalText: Theme.crtNativePath ? "" : (Browse.SystemsModel.count > 0 ? qsTr("%1 systems").arg(Browse.SystemsModel.count) : "") + totalPages: systems._bottomStatusVisible ? 1 : Math.max(1, Math.ceil(Browse.SystemsModel.count / systemsGrid.pageSize)) + totalText: systems._bottomStatusVisible ? "" : (Browse.SystemsModel.count > 0 ? qsTr("%1 systems").arg(Browse.SystemsModel.count) : "") rightTextOverride: !systems._listLayout || systemsGrid.itemCount <= 0 ? "" : qsTr("%1 / %2").arg(systemsGrid.currentIndex + 1).arg(Math.max(1, Browse.SystemsModel.count)) - visible: !systems.transitioning && (systems._tileLayout.showTopStrip || systems._crtListStrip) + visible: !systems.transitioning && BrowseLayouts.boolValue(systems._viewProfile, "status.topStripVisible", true) } BrowseListDetailView { @@ -194,13 +199,14 @@ Item { visible: !systems.transitioning && systems._listLayout anchors.left: parent.left - anchors.leftMargin: systems._tileLayout.listCardSideMargin + anchors.leftMargin: BrowseLayouts.numberValue(systems._viewProfile, "list.cardSideMargin", Sizing.pctW(5)) anchors.right: parent.right - anchors.rightMargin: systems._tileLayout.listCardSideMargin + anchors.rightMargin: BrowseLayouts.numberValue(systems._viewProfile, "list.cardSideMargin", Sizing.pctW(5)) anchors.top: topStrip.bottom anchors.topMargin: Sizing.pctH(2) anchors.bottom: parent.bottom anchors.bottomMargin: Sizing.pctH(8) + layoutProfile: systems._viewProfile model: Browse.SystemsModel currentIndex: systemsGrid.currentIndex detailTitle: listCard.currentName @@ -230,12 +236,12 @@ Item { anchors.right: parent.right anchors.top: topStrip.bottom anchors.bottom: parent.bottom - anchors.bottomMargin: systems._tileLayout.showBottomStatusRow ? systems._tileLayout.activeLabelBottomMargin + systems._tileLayout.activeLabelHeight : Sizing.pctH(6) + systems._tileLayout.activeLabelBottomMargin + systems._tileLayout.activeLabelHeight + anchors.bottomMargin: systems._gridBottomMargin focused: systems.gridFocused model: Browse.SystemsModel - layoutProfile: systems._tileLayout + layoutProfile: systems._gridProfile delegate: Tile { - layoutProfile: systems._tileLayout + layoutProfile: systems._gridProfile } onItemHovered: index => systems._focusIndex(index) onItemClicked: index => { @@ -264,18 +270,18 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - anchors.bottomMargin: systems._tileLayout.activeLabelBottomMargin - height: systems._tileLayout.activeLabelHeight + anchors.bottomMargin: systems._activeLabelBottomMargin + height: systems._activeLabelHeight text: systemsGrid.itemCount > 0 ? Browse.SystemsModel.system_name_at(systemsGrid.currentIndex) : "" visible: !systems.transitioning && !systems._listLayout } Text { - visible: systems._tileLayout.showBottomStatusRow && !systems.transitioning && !systems._listLayout && Browse.SystemsModel.count > 0 + visible: systems._bottomStatusVisible && !systems.transitioning && !systems._listLayout && Browse.SystemsModel.count > 0 anchors.left: parent.left - anchors.leftMargin: systems._tileLayout.bottomStatusLeftMargin + anchors.leftMargin: systems._bottomStatusLeftMargin anchors.verticalCenter: activeLabel.verticalCenter - width: Sizing.px(parent.width / 3) - systems._tileLayout.bottomStatusLeftMargin + width: Sizing.px(parent.width / 3) - systems._bottomStatusLeftMargin height: Sizing.fontSize(2.9) elide: Text.ElideRight horizontalAlignment: Text.AlignLeft @@ -288,11 +294,11 @@ Item { } Text { - visible: systems._tileLayout.showBottomStatusRow && !systems.transitioning && !systems._listLayout && Math.ceil(Browse.SystemsModel.count / systemsGrid.pageSize) > 1 + visible: systems._bottomStatusVisible && !systems.transitioning && !systems._listLayout && Math.ceil(Browse.SystemsModel.count / systemsGrid.pageSize) > 1 anchors.right: parent.right - anchors.rightMargin: systems._tileLayout.bottomStatusRightMargin + anchors.rightMargin: systems._bottomStatusRightMargin anchors.verticalCenter: activeLabel.verticalCenter - width: Sizing.px(parent.width / 3) - systems._tileLayout.bottomStatusRightMargin + width: Sizing.px(parent.width / 3) - systems._bottomStatusRightMargin height: Sizing.fontSize(2.9) elide: Text.ElideRight horizontalAlignment: Text.AlignRight diff --git a/src/ui/theme/BrowseLayouts.qml b/src/ui/theme/BrowseLayouts.qml index 4b96161..b3ac0f4 100644 --- a/src/ui/theme/BrowseLayouts.qml +++ b/src/ui/theme/BrowseLayouts.qml @@ -3,126 +3,120 @@ // SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 pragma Singleton import QtQuick - -// Shared browse-screen layout profiles. These only describe geometry and -// slot placement; screen behavior stays in the screens/components that -// consume them. +// Browse-theme profiles loaded from bundled JSON. The theme files stay +// data-only; this singleton maps their semantic sections (`header`, +// `status`, `grid`, `list`, `detail`, `footer`, `surface`) into the +// QML tree with lightweight token resolution against `Sizing`. QtObject { - readonly property QtObject defaultTile: QtObject { - readonly property bool showTopStrip: true - readonly property bool showHeaderTitleInHeader: false - readonly property bool showBottomStatusRow: false - readonly property bool headerHudBottomAligned: false - readonly property bool headerStatusPillPinnedTop: false - readonly property int gridLeftInset: Sizing.pctW(5) - readonly property int gridRightInset: Sizing.pctW(5) - readonly property int gridGutterWidth: Sizing.pctW(3) - readonly property int gridGutterGap: Sizing.pctW(1.5) - readonly property int gridColumnGap: Sizing.pctW(3) - readonly property int gridTopInset: Sizing.pctH(2) - readonly property int gridBottomInset: Sizing.pctH(2) - readonly property int gridRowGap: Sizing.pctH(4) - readonly property int scrollThumbWidth: Sizing.pctW(1.2) - readonly property int scrollThumbRightInset: 0 - readonly property bool scrollThumbRightAligned: false - readonly property int scrollArrowSize: Math.min(gridGutterWidth, Sizing.pctH(4)) - readonly property bool packHorizontalRemainderAfterGutter: false - readonly property int activeLabelHeight: Sizing.pctH(7) - readonly property int activeLabelBottomMargin: Sizing.pctH(8) - readonly property int bottomStatusLeftMargin: Sizing.pctW(5) - readonly property int bottomStatusRightMargin: Sizing.pctW(5) - readonly property int bottomUnsafeHeight: Sizing.pctH(6) + Sizing.pctH(2) - readonly property int tileCornerRadius: Sizing.cornerRadius - readonly property int listCardSideMargin: Sizing.pctW(5) - readonly property int listDividerOffsetX: 0 - readonly property int listStripHeight: Sizing.pctH(7) - readonly property int listStripSlotMargin: Sizing.pctW(5) - readonly property int listCardPaddingLeft: Sizing.pctW(2) - readonly property int listCardPaddingRight: Sizing.pctW(2) - readonly property int listCardPaddingTop: Sizing.pctH(2) - readonly property int listCardPaddingBottom: Sizing.pctH(2) - readonly property int listRowHeight: 0 - readonly property int listRowSpacing: Sizing.pctH(0.7) - readonly property int listCenterSlot: -1 - readonly property int listScrollbarGap: Sizing.pctW(1.5) - readonly property int listSelectionAccentWidth: Sizing.pctW(0.45) - readonly property int detailMetadataYOffset: 0 - readonly property int detailMetadataExtraHeight: 0 - readonly property int detailMetadataLeftInset: 0 - readonly property int detailMetadataRightInset: 0 - readonly property int detailPanePaddingLeft: Sizing.pctW(2) - readonly property int detailPanePaddingRight: Sizing.pctW(2) - readonly property int detailPanePaddingTop: Sizing.pctH(2) - readonly property int detailPanePaddingBottom: Sizing.pctH(2) - readonly property int detailImageXOffset: 0 - readonly property int detailImageLeftInset: 0 - readonly property int detailImageRightInset: 0 - readonly property int detailImageExtraWidth: 0 - readonly property int detailImageExtraHeight: 0 - readonly property int detailImageBottomGap: 0 - readonly property int detailTagRowHeight: Sizing.pctH(3) - readonly property int detailTagRowSpacing: Sizing.pctH(0.55) - readonly property int listRowTextLeftPadding: Sizing.pctW(1.6) - readonly property int listRowTextRightPadding: Sizing.pctW(1.6) - readonly property int listFavoriteRightPadding: Sizing.pctW(1.6) + readonly property string currentThemeId: ThemePalette.currentThemeId + + function currentProfile(viewId: string): var { + return BrowseLayouts.themeProfile(BrowseLayouts.currentThemeId, viewId); + } + + function themeProfile(themeId: string, viewId: string): var { + const themeData = ThemePalette.themeData(themeId); + if (themeData === null || typeof themeData !== "object") + return null; + if (!(viewId in themeData)) { + console.warn("BrowseLayouts: missing view profile '" + viewId + "' in theme '" + themeId + "'"); + return null; + } + return themeData[viewId]; + } + + function hasValue(profile: var, path: string): bool { + return BrowseLayouts._lookup(profile, path) !== undefined; + } + + function boolValue(profile: var, path: string, fallback: bool): bool { + const resolved = BrowseLayouts._resolve(profile, BrowseLayouts._lookup(profile, path), {}); + return typeof resolved === "boolean" ? resolved : fallback; + } + + function numberValue(profile: var, path: string, fallback: int): int { + const resolved = BrowseLayouts._resolve(profile, BrowseLayouts._lookup(profile, path), {}); + return typeof resolved === "number" && isFinite(resolved) ? resolved : fallback; } - readonly property QtObject crtTile: QtObject { - readonly property bool showTopStrip: false - readonly property bool showHeaderTitleInHeader: true - readonly property bool showBottomStatusRow: true - readonly property bool headerHudBottomAligned: true - readonly property bool headerStatusPillPinnedTop: true - readonly property int gridLeftInset: 4 - readonly property int gridRightInset: 0 - readonly property int gridGutterWidth: 8 - readonly property int gridGutterGap: 4 - readonly property int gridColumnGap: 4 - readonly property int gridTopInset: 2 - readonly property int gridBottomInset: 4 - readonly property int gridRowGap: 4 - readonly property int scrollThumbWidth: 4 - readonly property int scrollThumbRightInset: 2 - readonly property bool scrollThumbRightAligned: false - readonly property int scrollArrowSize: 8 - readonly property bool packHorizontalRemainderAfterGutter: true - readonly property int activeLabelHeight: 8 - readonly property int activeLabelBottomMargin: Sizing.pctH(6) - readonly property int bottomStatusLeftMargin: 4 - readonly property int bottomStatusRightMargin: Sizing.pctW(5) - readonly property int bottomUnsafeHeight: 16 - readonly property int tileCornerRadius: 4 - readonly property int listCardSideMargin: 4 - readonly property int listDividerOffsetX: -16 - readonly property int listStripHeight: 8 - readonly property int listStripSlotMargin: Sizing.headerSideMargin - readonly property int listCardPaddingLeft: 3 - readonly property int listCardPaddingRight: 2 - readonly property int listCardPaddingTop: 3 - readonly property int listCardPaddingBottom: 2 - readonly property int listRowHeight: 12 - readonly property int listRowSpacing: 0 - readonly property int listCenterSlot: 7 - readonly property int listScrollbarGap: 2 - readonly property int listSelectionAccentWidth: 2 - readonly property int detailMetadataYOffset: -14 - readonly property int detailMetadataExtraHeight: 2 - readonly property int detailMetadataLeftInset: 2 - readonly property int detailMetadataRightInset: 1 - readonly property int detailPanePaddingLeft: 1 - readonly property int detailPanePaddingRight: 1 - readonly property int detailPanePaddingTop: 3 - readonly property int detailPanePaddingBottom: 2 - readonly property int detailImageXOffset: 0 - readonly property int detailImageLeftInset: 2 - readonly property int detailImageRightInset: 2 - readonly property int detailImageExtraWidth: 16 - readonly property int detailImageExtraHeight: 0 - readonly property int detailImageBottomGap: 2 - readonly property int detailTagRowHeight: 9 - readonly property int detailTagRowSpacing: 0 - readonly property int listRowTextLeftPadding: 4 - readonly property int listRowTextRightPadding: 2 - readonly property int listFavoriteRightPadding: 2 + function stringValue(profile: var, path: string, fallback: string): string { + const resolved = BrowseLayouts._resolve(profile, BrowseLayouts._lookup(profile, path), {}); + return typeof resolved === "string" ? resolved : fallback; + } + + function _lookup(profile: var, path: string): var { + if (profile === null || typeof profile !== "object" || path === "") + return undefined; + const parts = path.split("."); + let current = profile; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (current === null || typeof current !== "object" || !(part in current)) + return undefined; + current = current[part]; + } + return current; + } + + function _splitArgs(argsText: string): var { + const parts = []; + let start = 0; + let depth = 0; + for (let i = 0; i < argsText.length; i++) { + const ch = argsText[i]; + if (ch === "(") + depth++; + else if (ch === ")") + depth--; + else if (ch === "," && depth === 0) { + parts.push(argsText.substring(start, i).trim()); + start = i + 1; + } + } + parts.push(argsText.substring(start).trim()); + return parts; + } + + function _resolve(profile: var, value: var, seenRefs: var): var { + if (typeof value !== "string") + return value; + + if (value.startsWith("pctW:")) + return Sizing.pctW(Number(value.substring("pctW:".length))); + if (value.startsWith("pctH:")) + return Sizing.pctH(Number(value.substring("pctH:".length))); + if (value.startsWith("fontSize:")) + return Sizing.fontSize(Number(value.substring("fontSize:".length))); + if (value === "cornerRadius") + return Sizing.cornerRadius; + if (value === "headerSideMargin") + return Sizing.headerSideMargin; + if (value.startsWith("ref:")) { + const refPath = value.substring("ref:".length); + if (seenRefs[refPath] === true) { + console.warn("BrowseLayouts: circular ref '" + refPath + "'"); + return undefined; + } + seenRefs[refPath] = true; + return BrowseLayouts._resolve(profile, BrowseLayouts._lookup(profile, refPath), seenRefs); + } + + const fnMatch = value.match(/^([a-z]+)\((.*)\)$/); + if (fnMatch !== null) { + const fnName = fnMatch[1]; + const args = BrowseLayouts._splitArgs(fnMatch[2]).map(arg => BrowseLayouts._resolve(profile, arg, seenRefs)); + if (args.length === 2 && fnName === "min") + return Math.min(args[0], args[1]); + if (args.length === 2 && fnName === "max") + return Math.max(args[0], args[1]); + if (fnName === "sum") + return args.reduce((total, arg) => total + arg, 0); + } + + const numeric = Number(value); + if (!isNaN(numeric)) + return numeric; + + return value; } } diff --git a/src/ui/theme/CMakeLists.txt b/src/ui/theme/CMakeLists.txt index bc88c9f..1f317dd 100644 --- a/src/ui/theme/CMakeLists.txt +++ b/src/ui/theme/CMakeLists.txt @@ -9,6 +9,7 @@ set_source_files_properties( Resources.qml Sizing.qml Theme.qml + ThemePalette.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE ) @@ -21,6 +22,10 @@ qt_add_qml_module( Resources.qml Sizing.qml Theme.qml + ThemePalette.qml + RESOURCES + browse-themes/default.json + browse-themes/crt.json ) target_link_libraries( diff --git a/src/ui/theme/Theme.qml b/src/ui/theme/Theme.qml index ee1dd21..b3f91f6 100644 --- a/src/ui/theme/Theme.qml +++ b/src/ui/theme/Theme.qml @@ -7,6 +7,7 @@ import QtQuick // Project-wide color and font constants. // Never hardcode colors or font families inline — use these instead. QtObject { + property string currentThemeId: "default" property bool crtNativePath: false // Backgrounds diff --git a/src/ui/theme/ThemePalette.qml b/src/ui/theme/ThemePalette.qml new file mode 100644 index 0000000..088c395 --- /dev/null +++ b/src/ui/theme/ThemePalette.qml @@ -0,0 +1,79 @@ +// Zaparoo Frontend +// Copyright (c) 2026 Wizzo Pty Ltd and the Zaparoo Project contributors. +// SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 +pragma Singleton +import QtQuick +import Zaparoo.Browse as Browse + +// Shared theme-data loader. Owns JSON loading and exposes both palette +// lookups and full theme objects so visual singletons/components do not +// each grow their own loader path. +QtObject { + property bool crtNativePath: false + readonly property string currentThemeId: crtNativePath ? "crt" : Browse.Settings.current_theme + property var _themeCache: ({}) + + function themeData(themeId: string): var { + return ThemePalette._loadTheme(themeId); + } + + function currentTheme(): var { + return ThemePalette.themeData(ThemePalette.currentThemeId); + } + + function colorValue(path: string, fallback: color): color { + const resolved = ThemePalette._lookup(ThemePalette.currentTheme(), path); + return typeof resolved === "string" && resolved !== "" ? resolved : fallback; + } + + function _themeUrl(themeId: string): string { + if (themeId === "default" || themeId === "crt") + return "qrc:/qt/qml/Zaparoo/Theme/browse-themes/" + themeId + ".json"; + const base = Browse.Settings.themes_directory_url; + return base !== "" ? base + themeId + ".json" : ""; + } + + function _loadTheme(themeId: string): var { + if (ThemePalette._themeCache[themeId] !== undefined) + return ThemePalette._themeCache[themeId]; + + const url = ThemePalette._themeUrl(themeId); + if (url === "") { + ThemePalette._themeCache[themeId] = null; + return null; + } + + const req = new XMLHttpRequest(); + req.open("GET", url, false); + req.send(); + + if (req.status !== 0 && (req.status < 200 || req.status >= 300)) { + console.warn("ThemePalette: failed to load theme '" + themeId + "' from " + url + " (status " + req.status + ")"); + ThemePalette._themeCache[themeId] = null; + return null; + } + + try { + ThemePalette._themeCache[themeId] = JSON.parse(req.responseText); + } catch (err) { + console.warn("ThemePalette: invalid JSON in theme '" + themeId + "': " + err); + ThemePalette._themeCache[themeId] = null; + } + + return ThemePalette._themeCache[themeId]; + } + + function _lookup(themeData: var, path: string): var { + if (themeData === null || typeof themeData !== "object" || path === "") + return undefined; + const parts = path.split("."); + let current = themeData; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (current === null || typeof current !== "object" || !(part in current)) + return undefined; + current = current[part]; + } + return current; + } +} diff --git a/src/ui/theme/browse-themes/crt.json b/src/ui/theme/browse-themes/crt.json new file mode 100644 index 0000000..06a56ad --- /dev/null +++ b/src/ui/theme/browse-themes/crt.json @@ -0,0 +1,303 @@ +{ + "palette": { + "bgDeep": "#0f0f23", + "bgPanel": "#1a1a35", + "bgBar": "#0a0a15", + "surfaceCard": "#2a2a45", + "selectionSurface": "#3a3a66", + "scrim": "#cc000000", + "borderSubtle": "#1a1a2e", + "borderMid": "#404060", + "textPrimary": "#ffffff", + "textLabel": "#888888", + "accent": "#FFB347" + }, + "systemsGrid": { + "header": { + "titleInHeader": true, + "hudBottomAligned": true, + "statusPillPinnedTop": true + }, + "status": { + "topStripVisible": false, + "stripHeight": 0, + "slotMargin": "headerSideMargin" + }, + "grid": { + "leftInset": 4, + "rightInset": 0, + "gutterWidth": 8, + "gutterGap": 4, + "columnGap": 4, + "topInset": 2, + "bottomInset": 4, + "rowGap": 4, + "scrollThumbWidth": 4, + "scrollThumbRightInset": 2, + "scrollThumbRightAligned": false, + "scrollArrowSize": 8, + "packHorizontalRemainderAfterGutter": true + }, + "list": { + "cardSideMargin": 4, + "dividerOffsetX": -16, + "cardPaddingLeft": 3, + "cardPaddingRight": 2, + "cardPaddingTop": 3, + "cardPaddingBottom": 2, + "rowHeight": 12, + "rowSpacing": 0, + "centerSlot": 7, + "scrollbarGap": 2, + "selectionAccentWidth": 2, + "rowTextLeftPadding": 4, + "rowTextRightPadding": 2, + "favoriteRightPadding": 2 + }, + "detail": { + "metadataYOffset": -14, + "metadataExtraHeight": 2, + "metadataLeftInset": 2, + "metadataRightInset": 1, + "panePaddingLeft": 1, + "panePaddingRight": 1, + "panePaddingTop": 3, + "panePaddingBottom": 2, + "imageXOffset": 0, + "imageLeftInset": 2, + "imageRightInset": 2, + "imageExtraWidth": 16, + "imageExtraHeight": 0, + "imageBottomGap": 2, + "tagRowHeight": 9, + "tagRowSpacing": 0 + }, + "footer": { + "activeLabelHeight": 8, + "activeLabelBottomMargin": "pctH:6", + "bottomStatusVisible": true, + "bottomStatusLeftMargin": 4, + "bottomStatusRightMargin": "pctW:5", + "bottomUnsafeHeight": 16 + }, + "surface": { + "cornerRadius": 4 + } + }, + "systemsList": { + "header": { + "titleInHeader": true, + "hudBottomAligned": true, + "statusPillPinnedTop": true + }, + "status": { + "topStripVisible": true, + "stripHeight": 8, + "slotMargin": "headerSideMargin" + }, + "grid": { + "leftInset": 4, + "rightInset": 0, + "gutterWidth": 8, + "gutterGap": 4, + "columnGap": 4, + "topInset": 2, + "bottomInset": 4, + "rowGap": 4, + "scrollThumbWidth": 4, + "scrollThumbRightInset": 2, + "scrollThumbRightAligned": false, + "scrollArrowSize": 8, + "packHorizontalRemainderAfterGutter": true + }, + "list": { + "cardSideMargin": 4, + "dividerOffsetX": -16, + "cardPaddingLeft": 3, + "cardPaddingRight": 2, + "cardPaddingTop": 3, + "cardPaddingBottom": 2, + "rowHeight": 12, + "rowSpacing": 0, + "centerSlot": 7, + "scrollbarGap": 2, + "selectionAccentWidth": 2, + "rowTextLeftPadding": 4, + "rowTextRightPadding": 2, + "favoriteRightPadding": 2 + }, + "detail": { + "metadataYOffset": -14, + "metadataExtraHeight": 2, + "metadataLeftInset": 2, + "metadataRightInset": 1, + "panePaddingLeft": 1, + "panePaddingRight": 1, + "panePaddingTop": 3, + "panePaddingBottom": 2, + "imageXOffset": 0, + "imageLeftInset": 2, + "imageRightInset": 2, + "imageExtraWidth": 16, + "imageExtraHeight": 0, + "imageBottomGap": 2, + "tagRowHeight": 9, + "tagRowSpacing": 0 + }, + "footer": { + "activeLabelHeight": 8, + "activeLabelBottomMargin": "pctH:6", + "bottomStatusVisible": false, + "bottomStatusLeftMargin": 4, + "bottomStatusRightMargin": "pctW:5", + "bottomUnsafeHeight": 16 + }, + "surface": { + "cornerRadius": 4 + } + }, + "gamesGrid": { + "header": { + "titleInHeader": true, + "hudBottomAligned": true, + "statusPillPinnedTop": true + }, + "status": { + "topStripVisible": false, + "stripHeight": 0, + "slotMargin": "headerSideMargin" + }, + "grid": { + "leftInset": 4, + "rightInset": 0, + "gutterWidth": 8, + "gutterGap": 4, + "columnGap": 4, + "topInset": 2, + "bottomInset": 4, + "rowGap": 4, + "scrollThumbWidth": 4, + "scrollThumbRightInset": 2, + "scrollThumbRightAligned": false, + "scrollArrowSize": 8, + "packHorizontalRemainderAfterGutter": true + }, + "list": { + "cardSideMargin": 4, + "dividerOffsetX": -16, + "cardPaddingLeft": 3, + "cardPaddingRight": 2, + "cardPaddingTop": 3, + "cardPaddingBottom": 2, + "rowHeight": 12, + "rowSpacing": 0, + "centerSlot": 7, + "scrollbarGap": 2, + "selectionAccentWidth": 2, + "rowTextLeftPadding": 4, + "rowTextRightPadding": 2, + "favoriteRightPadding": 2 + }, + "detail": { + "metadataYOffset": -14, + "metadataExtraHeight": 2, + "metadataLeftInset": 2, + "metadataRightInset": 1, + "panePaddingLeft": 1, + "panePaddingRight": 1, + "panePaddingTop": 3, + "panePaddingBottom": 2, + "imageXOffset": 0, + "imageLeftInset": 2, + "imageRightInset": 2, + "imageExtraWidth": 16, + "imageExtraHeight": 0, + "imageBottomGap": 2, + "tagRowHeight": 9, + "tagRowSpacing": 0 + }, + "footer": { + "activeLabelHeight": 8, + "activeLabelBottomMargin": "pctH:6", + "bottomStatusVisible": true, + "bottomStatusLeftMargin": 4, + "bottomStatusRightMargin": "pctW:5", + "bottomUnsafeHeight": 16 + }, + "surface": { + "cornerRadius": 4 + } + }, + "gamesList": { + "header": { + "titleInHeader": true, + "hudBottomAligned": true, + "statusPillPinnedTop": true + }, + "status": { + "topStripVisible": true, + "stripHeight": 8, + "slotMargin": "headerSideMargin" + }, + "grid": { + "leftInset": 4, + "rightInset": 0, + "gutterWidth": 8, + "gutterGap": 4, + "columnGap": 4, + "topInset": 2, + "bottomInset": 4, + "rowGap": 4, + "scrollThumbWidth": 4, + "scrollThumbRightInset": 2, + "scrollThumbRightAligned": false, + "scrollArrowSize": 8, + "packHorizontalRemainderAfterGutter": true + }, + "list": { + "cardSideMargin": 4, + "dividerOffsetX": -16, + "cardPaddingLeft": 3, + "cardPaddingRight": 2, + "cardPaddingTop": 3, + "cardPaddingBottom": 2, + "rowHeight": 12, + "rowSpacing": 0, + "centerSlot": 7, + "scrollbarGap": 2, + "selectionAccentWidth": 2, + "rowTextLeftPadding": 4, + "rowTextRightPadding": 2, + "favoriteRightPadding": 2 + }, + "detail": { + "metadataYOffset": -14, + "metadataExtraHeight": 2, + "metadataLeftInset": 2, + "metadataRightInset": 1, + "panePaddingLeft": 1, + "panePaddingRight": 1, + "panePaddingTop": 3, + "panePaddingBottom": 2, + "imageXOffset": 0, + "imageLeftInset": 2, + "imageRightInset": 2, + "imageExtraWidth": 16, + "imageExtraHeight": 0, + "imageBottomGap": 2, + "tagRowHeight": 9, + "tagRowSpacing": 0 + }, + "footer": { + "activeLabelHeight": 8, + "activeLabelBottomMargin": "pctH:6", + "bottomStatusVisible": false, + "bottomStatusLeftMargin": 4, + "bottomStatusRightMargin": "pctW:5", + "bottomUnsafeHeight": 16 + }, + "surface": { + "cornerRadius": 4 + } + } +} diff --git a/src/ui/theme/browse-themes/default.json b/src/ui/theme/browse-themes/default.json new file mode 100644 index 0000000..02835c0 --- /dev/null +++ b/src/ui/theme/browse-themes/default.json @@ -0,0 +1,303 @@ +{ + "palette": { + "bgDeep": "#0f0f23", + "bgPanel": "#1a1a35", + "bgBar": "#0a0a15", + "surfaceCard": "#2a2a45", + "selectionSurface": "#3a3a66", + "scrim": "#cc000000", + "borderSubtle": "#1a1a2e", + "borderMid": "#404060", + "textPrimary": "#ffffff", + "textLabel": "#888888", + "accent": "#FFB347" + }, + "systemsGrid": { + "header": { + "titleInHeader": false, + "hudBottomAligned": false, + "statusPillPinnedTop": false + }, + "status": { + "topStripVisible": true, + "stripHeight": "pctH:7", + "slotMargin": "pctW:5" + }, + "grid": { + "leftInset": "pctW:5", + "rightInset": "pctW:5", + "gutterWidth": "pctW:3", + "gutterGap": "pctW:1.5", + "columnGap": "pctW:3", + "topInset": "pctH:2", + "bottomInset": "pctH:2", + "rowGap": "pctH:4", + "scrollThumbWidth": "pctW:1.2", + "scrollThumbRightInset": 0, + "scrollThumbRightAligned": false, + "scrollArrowSize": "min(ref:grid.gutterWidth,pctH:4)", + "packHorizontalRemainderAfterGutter": false + }, + "list": { + "cardSideMargin": "pctW:5", + "dividerOffsetX": 0, + "cardPaddingLeft": "pctW:2", + "cardPaddingRight": "pctW:2", + "cardPaddingTop": "pctH:2", + "cardPaddingBottom": "pctH:2", + "rowHeight": 0, + "rowSpacing": "pctH:0.7", + "centerSlot": -1, + "scrollbarGap": "pctW:1.5", + "selectionAccentWidth": "pctW:0.45", + "rowTextLeftPadding": "pctW:1.6", + "rowTextRightPadding": "pctW:1.6", + "favoriteRightPadding": "pctW:1.6" + }, + "detail": { + "metadataYOffset": 0, + "metadataExtraHeight": 0, + "metadataLeftInset": 0, + "metadataRightInset": 0, + "panePaddingLeft": "pctW:2", + "panePaddingRight": "pctW:2", + "panePaddingTop": "pctH:2", + "panePaddingBottom": "pctH:2", + "imageXOffset": 0, + "imageLeftInset": 0, + "imageRightInset": 0, + "imageExtraWidth": 0, + "imageExtraHeight": 0, + "imageBottomGap": 0, + "tagRowHeight": "pctH:3", + "tagRowSpacing": "pctH:0.55" + }, + "footer": { + "activeLabelHeight": "pctH:7", + "activeLabelBottomMargin": "pctH:8", + "bottomStatusVisible": false, + "bottomStatusLeftMargin": "pctW:5", + "bottomStatusRightMargin": "pctW:5", + "bottomUnsafeHeight": "sum(pctH:6,pctH:2)" + }, + "surface": { + "cornerRadius": "cornerRadius" + } + }, + "systemsList": { + "header": { + "titleInHeader": false, + "hudBottomAligned": false, + "statusPillPinnedTop": false + }, + "status": { + "topStripVisible": true, + "stripHeight": "pctH:7", + "slotMargin": "pctW:5" + }, + "grid": { + "leftInset": "pctW:5", + "rightInset": "pctW:5", + "gutterWidth": "pctW:3", + "gutterGap": "pctW:1.5", + "columnGap": "pctW:3", + "topInset": "pctH:2", + "bottomInset": "pctH:2", + "rowGap": "pctH:4", + "scrollThumbWidth": "pctW:1.2", + "scrollThumbRightInset": 0, + "scrollThumbRightAligned": false, + "scrollArrowSize": "min(ref:grid.gutterWidth,pctH:4)", + "packHorizontalRemainderAfterGutter": false + }, + "list": { + "cardSideMargin": "pctW:5", + "dividerOffsetX": 0, + "cardPaddingLeft": "pctW:2", + "cardPaddingRight": "pctW:2", + "cardPaddingTop": "pctH:2", + "cardPaddingBottom": "pctH:2", + "rowHeight": 0, + "rowSpacing": "pctH:0.7", + "centerSlot": -1, + "scrollbarGap": "pctW:1.5", + "selectionAccentWidth": "pctW:0.45", + "rowTextLeftPadding": "pctW:1.6", + "rowTextRightPadding": "pctW:1.6", + "favoriteRightPadding": "pctW:1.6" + }, + "detail": { + "metadataYOffset": 0, + "metadataExtraHeight": 0, + "metadataLeftInset": 0, + "metadataRightInset": 0, + "panePaddingLeft": "pctW:2", + "panePaddingRight": "pctW:2", + "panePaddingTop": "pctH:2", + "panePaddingBottom": "pctH:2", + "imageXOffset": 0, + "imageLeftInset": 0, + "imageRightInset": 0, + "imageExtraWidth": 0, + "imageExtraHeight": 0, + "imageBottomGap": 0, + "tagRowHeight": "pctH:3", + "tagRowSpacing": "pctH:0.55" + }, + "footer": { + "activeLabelHeight": "pctH:7", + "activeLabelBottomMargin": "pctH:8", + "bottomStatusVisible": false, + "bottomStatusLeftMargin": "pctW:5", + "bottomStatusRightMargin": "pctW:5", + "bottomUnsafeHeight": "sum(pctH:6,pctH:2)" + }, + "surface": { + "cornerRadius": "cornerRadius" + } + }, + "gamesGrid": { + "header": { + "titleInHeader": false, + "hudBottomAligned": false, + "statusPillPinnedTop": false + }, + "status": { + "topStripVisible": true, + "stripHeight": "pctH:7", + "slotMargin": "pctW:5" + }, + "grid": { + "leftInset": "pctW:5", + "rightInset": "pctW:5", + "gutterWidth": "pctW:3", + "gutterGap": "pctW:1.5", + "columnGap": "pctW:3", + "topInset": "pctH:2", + "bottomInset": "pctH:2", + "rowGap": "pctH:4", + "scrollThumbWidth": "pctW:1.2", + "scrollThumbRightInset": 0, + "scrollThumbRightAligned": false, + "scrollArrowSize": "min(ref:grid.gutterWidth,pctH:4)", + "packHorizontalRemainderAfterGutter": false + }, + "list": { + "cardSideMargin": "pctW:5", + "dividerOffsetX": 0, + "cardPaddingLeft": "pctW:2", + "cardPaddingRight": "pctW:2", + "cardPaddingTop": "pctH:2", + "cardPaddingBottom": "pctH:2", + "rowHeight": 0, + "rowSpacing": "pctH:0.7", + "centerSlot": -1, + "scrollbarGap": "pctW:1.5", + "selectionAccentWidth": "pctW:0.45", + "rowTextLeftPadding": "pctW:1.6", + "rowTextRightPadding": "pctW:1.6", + "favoriteRightPadding": "pctW:1.6" + }, + "detail": { + "metadataYOffset": 0, + "metadataExtraHeight": 0, + "metadataLeftInset": 0, + "metadataRightInset": 0, + "panePaddingLeft": "pctW:2", + "panePaddingRight": "pctW:2", + "panePaddingTop": "pctH:2", + "panePaddingBottom": "pctH:2", + "imageXOffset": 0, + "imageLeftInset": 0, + "imageRightInset": 0, + "imageExtraWidth": 0, + "imageExtraHeight": 0, + "imageBottomGap": 0, + "tagRowHeight": "pctH:3", + "tagRowSpacing": "pctH:0.55" + }, + "footer": { + "activeLabelHeight": "pctH:7", + "activeLabelBottomMargin": "pctH:8", + "bottomStatusVisible": false, + "bottomStatusLeftMargin": "pctW:5", + "bottomStatusRightMargin": "pctW:5", + "bottomUnsafeHeight": "sum(pctH:6,pctH:2)" + }, + "surface": { + "cornerRadius": "cornerRadius" + } + }, + "gamesList": { + "header": { + "titleInHeader": false, + "hudBottomAligned": false, + "statusPillPinnedTop": false + }, + "status": { + "topStripVisible": true, + "stripHeight": "pctH:7", + "slotMargin": "pctW:5" + }, + "grid": { + "leftInset": "pctW:5", + "rightInset": "pctW:5", + "gutterWidth": "pctW:3", + "gutterGap": "pctW:1.5", + "columnGap": "pctW:3", + "topInset": "pctH:2", + "bottomInset": "pctH:2", + "rowGap": "pctH:4", + "scrollThumbWidth": "pctW:1.2", + "scrollThumbRightInset": 0, + "scrollThumbRightAligned": false, + "scrollArrowSize": "min(ref:grid.gutterWidth,pctH:4)", + "packHorizontalRemainderAfterGutter": false + }, + "list": { + "cardSideMargin": "pctW:5", + "dividerOffsetX": 0, + "cardPaddingLeft": "pctW:2", + "cardPaddingRight": "pctW:2", + "cardPaddingTop": "pctH:2", + "cardPaddingBottom": "pctH:2", + "rowHeight": 0, + "rowSpacing": "pctH:0.7", + "centerSlot": -1, + "scrollbarGap": "pctW:1.5", + "selectionAccentWidth": "pctW:0.45", + "rowTextLeftPadding": "pctW:1.6", + "rowTextRightPadding": "pctW:1.6", + "favoriteRightPadding": "pctW:1.6" + }, + "detail": { + "metadataYOffset": 0, + "metadataExtraHeight": 0, + "metadataLeftInset": 0, + "metadataRightInset": 0, + "panePaddingLeft": "pctW:2", + "panePaddingRight": "pctW:2", + "panePaddingTop": "pctH:2", + "panePaddingBottom": "pctH:2", + "imageXOffset": 0, + "imageLeftInset": 0, + "imageRightInset": 0, + "imageExtraWidth": 0, + "imageExtraHeight": 0, + "imageBottomGap": 0, + "tagRowHeight": "pctH:3", + "tagRowSpacing": "pctH:0.55" + }, + "footer": { + "activeLabelHeight": "pctH:7", + "activeLabelBottomMargin": "pctH:8", + "bottomStatusVisible": false, + "bottomStatusLeftMargin": "pctW:5", + "bottomStatusRightMargin": "pctW:5", + "bottomUnsafeHeight": "sum(pctH:6,pctH:2)" + }, + "surface": { + "cornerRadius": "cornerRadius" + } + } +} From 7f9e6782f7c99a0f371d9567a5767da0e35b6827 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Tue, 26 May 2026 00:47:52 +0200 Subject: [PATCH 2/2] pause work --- src/ui/app/MainLayout.qml | 48 +++++++++++++++++++++++++++++++++++ src/ui/theme/Theme.qml | 22 ++++++++-------- src/ui/theme/ThemePalette.qml | 32 ++++++++++++++--------- 3 files changed, 79 insertions(+), 23 deletions(-) diff --git a/src/ui/app/MainLayout.qml b/src/ui/app/MainLayout.qml index e6c5c49..64b227e 100644 --- a/src/ui/app/MainLayout.qml +++ b/src/ui/app/MainLayout.qml @@ -198,6 +198,54 @@ ApplicationWindow { value: root.crtNativePath || Browse.Settings.current_theme === "crt" } + Binding { + target: Theme + property: "bgDeep" + value: ThemePalette.colorValue(ThemePalette.currentThemeId, "palette.bgDeep", "#0f0f23") + } + + Binding { + target: Theme + property: "bgPanel" + value: ThemePalette.colorValue(ThemePalette.currentThemeId, "palette.bgPanel", "#1a1a35") + } + + Binding { + target: Theme + property: "bgBar" + value: ThemePalette.colorValue(ThemePalette.currentThemeId, "palette.bgBar", "#0a0a15") + } + + Binding { + target: Theme + property: "surfaceCard" + value: ThemePalette.colorValue(ThemePalette.currentThemeId, "palette.surfaceCard", "#2a2a45") + } + + Binding { + target: Theme + property: "selectionSurface" + value: ThemePalette.colorValue(ThemePalette.currentThemeId, "palette.selectionSurface", "#3a3a66") + } + + Binding { + target: Theme + property: "borderSubtle" + value: ThemePalette.colorValue(ThemePalette.currentThemeId, "palette.borderSubtle", "#1a1a2e") + } + + Binding { + target: Theme + property: "borderMid" + value: ThemePalette.colorValue(ThemePalette.currentThemeId, "palette.borderMid", "#404060") + } + + Binding { + target: Theme + property: "accent" + value: ThemePalette.colorValue(ThemePalette.currentThemeId, "palette.accent", "#FFB347") + } + Binding { target: Sizing property: "crtNativePath" diff --git a/src/ui/theme/Theme.qml b/src/ui/theme/Theme.qml index b3f91f6..3ffaf5d 100644 --- a/src/ui/theme/Theme.qml +++ b/src/ui/theme/Theme.qml @@ -11,30 +11,30 @@ QtObject { property bool crtNativePath: false // Backgrounds - readonly property color bgDeep: "#0f0f23" - readonly property color bgPanel: "#1a1a35" - readonly property color bgBar: "#0a0a15" + property color bgDeep: "#0f0f23" + property color bgPanel: "#1a1a35" + property color bgBar: "#0a0a15" // Card surface used for tile bodies in rows/grids. Sits a step // above bgPanel so a solid white icon+label silhouette has clear // contrast — the page bg pattern stays visible in the gaps between // tiles, and each tile reads as a self-contained chip. - readonly property color surfaceCard: "#2a2a45" + property color surfaceCard: "#2a2a45" // Selected row fill. Cooler and darker than the amber accent so // text stays high-contrast while the accent bar remains the focus // cue layered on top. - readonly property color selectionSurface: "#3a3a66" + property color selectionSurface: "#3a3a66" // Modal scrim — translucent black so the screen behind a modal // dims uniformly without a blur or shader pass. - readonly property color scrim: "#cc000000" + property color scrim: "#cc000000" // Borders - readonly property color borderSubtle: "#1a1a2e" - readonly property color borderMid: "#404060" + property color borderSubtle: "#1a1a2e" + property color borderMid: "#404060" // Text - readonly property color textPrimary: "#ffffff" - readonly property color textLabel: "#888888" + property color textPrimary: "#ffffff" + property color textLabel: "#888888" // Accent — static warm amber used for selection highlights. - readonly property color accent: "#FFB347" + property color accent: "#FFB347" // Fonts readonly property string fontUi: crtNativePath ? "MxPlus HP 100LX 6x8" : "Noto Sans" readonly property string fontMono: crtNativePath ? "MxPlus HP 100LX 6x8" : "monospace" diff --git a/src/ui/theme/ThemePalette.qml b/src/ui/theme/ThemePalette.qml index 088c395..9f4a1db 100644 --- a/src/ui/theme/ThemePalette.qml +++ b/src/ui/theme/ThemePalette.qml @@ -21,8 +21,8 @@ QtObject { return ThemePalette.themeData(ThemePalette.currentThemeId); } - function colorValue(path: string, fallback: color): color { - const resolved = ThemePalette._lookup(ThemePalette.currentTheme(), path); + function colorValue(themeId: string, path: string, fallback: color): color { + const resolved = ThemePalette._lookup(ThemePalette.themeData(themeId), path); return typeof resolved === "string" && resolved !== "" ? resolved : fallback; } @@ -34,14 +34,13 @@ QtObject { } function _loadTheme(themeId: string): var { - if (ThemePalette._themeCache[themeId] !== undefined) - return ThemePalette._themeCache[themeId]; - const url = ThemePalette._themeUrl(themeId); - if (url === "") { - ThemePalette._themeCache[themeId] = null; + const cached = ThemePalette._themeCache[themeId]; + if (cached !== undefined && cached.url === url) + return cached.data; + + if (url === "") return null; - } const req = new XMLHttpRequest(); req.open("GET", url, false); @@ -49,18 +48,27 @@ QtObject { if (req.status !== 0 && (req.status < 200 || req.status >= 300)) { console.warn("ThemePalette: failed to load theme '" + themeId + "' from " + url + " (status " + req.status + ")"); - ThemePalette._themeCache[themeId] = null; + ThemePalette._themeCache[themeId] = { + url: url, + data: null + }; return null; } try { - ThemePalette._themeCache[themeId] = JSON.parse(req.responseText); + ThemePalette._themeCache[themeId] = { + url: url, + data: JSON.parse(req.responseText) + }; } catch (err) { console.warn("ThemePalette: invalid JSON in theme '" + themeId + "': " + err); - ThemePalette._themeCache[themeId] = null; + ThemePalette._themeCache[themeId] = { + url: url, + data: null + }; } - return ThemePalette._themeCache[themeId]; + return ThemePalette._themeCache[themeId].data; } function _lookup(themeData: var, path: string): var {