add crypto

This commit is contained in:
zack 2025-07-22 20:21:21 -04:00
parent 90cbe489f6
commit af6a3bce3e
Signed by: zoey
GPG key ID: 81FB9FECDD6A33E2
120 changed files with 24616 additions and 462 deletions

18
.sops.yaml Normal file
View file

@ -0,0 +1,18 @@
keys:
- &personal_pgp_key 0x141576B17B4AE789
- &pc_main 0DCB1C584AECEB2674BB76C179FE3B714935CDAB
creation_rules:
# This rule applies to any file named 'secrets.yaml' directly in the 'secrets/' directory
# or 'secrets/github-deploy-key.yaml' etc.
- path_regex: "secrets/.*\\.yaml$"
key_groups:
- pgp:
- *personal_pgp_key
- *pc_main
# Add host keys for decryption on the target system
# sops-nix will automatically pick up the system's SSH host keys
# as decryption keys if enabled in your NixOS config.
# So you typically don't list them explicitly here unless you
# want to restrict it to specific fingerprints, which is rare.
# This part ensures your *personal* key can decrypt it.

569
flake.lock generated

File diff suppressed because it is too large Load diff

View file

@ -12,7 +12,7 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
nixos-stable.url = "github:nixos/nixpkgs/nixos-24.11";
nixos-stable.url = "github:nixos/nixpkgs/nixos-25.05";
home-manager = {
url = "github:nix-community/home-manager";
@ -39,6 +39,11 @@
agenix.url = "github:ryantm/agenix";
agenix.inputs.nixpkgs.follows = "nixpkgs";
firefox-addons = {
url = "gitlab:rycee/nur-expressions?dir=pkgs/firefox-addons";
inputs.nixpkgs.follows = "nixpkgs";
};
# to replace with sops-nix
sops-nix.url = "github:Mic92/sops-nix";
@ -114,7 +119,7 @@
posting.url = "github:jorikvanveen/posting-flake";
lix-module = {
url = "https://git.lix.systems/lix-project/nixos-module/archive/2.91.1-1.tar.gz";
url = "https://git.lix.systems/lix-project/nixos-module/archive/2.93.2-1.tar.gz";
inputs.nixpkgs.follows = "nixpkgs";
};
@ -143,7 +148,9 @@
niri.url = "github:sodiboo/niri-flake";
chaotic.url = "github:chaotic-cx/nyx/7b20daf110d06962ee99114220f2fb98cdc8673d";
chaotic.url = "github:chaotic-cx/nyx";
vpn-confinement.url = "github:Maroka-chan/VPN-Confinement";
};
outputs = inputs @ {self, ...}: let
@ -202,6 +209,8 @@
disko.nixosModules.disko
niri.nixosModules.niri
chaotic.nixosModules.default
vpn-confinement.nixosModules.default
];
systems.modules.earth = with inputs; [

View file

@ -44,8 +44,6 @@
catppuccin.fzf.enable = true;
home.packages = with pkgs; [
devenv
(discord.override {
withOpenASAR = true;
withVencord = true;

View file

@ -2,7 +2,6 @@
config,
inputs,
pkgs,
system,
lib,
...
}: {
@ -10,8 +9,7 @@
wms.niri.enable = true;
apps = {
web.librewolf.enable = true;
web.zen.setDefault = true;
web.zen.enable = true;
web.librewolf.setDefault = true;
tools.git.enable = true;
tools.tmux.enable = true;
@ -125,6 +123,13 @@
work.vpn.enable = true;
programs.obs-studio = {
enable = true;
package = pkgs.obs-studio.override {
cudaSupport = true;
};
};
programs.atuin = {
enable = true;
enableFishIntegration = config.programs.fish.enable;
@ -144,15 +149,15 @@
awscli2
monero-cli
devenv
zoom-us
pandoc
nexusmods-app-unfree
qt6Packages.qt5compat
libsForQt5.qt5.qtgraphicaleffects
kdePackages.qtdeclarative
prismlauncher
obs-studio
# prismlauncher
(discord.override {
withOpenASAR = true;
@ -174,10 +179,6 @@
heroic
cartridges
darktable
thunderbird
custom.nvidia-nsight
custom.enc
@ -226,8 +227,6 @@
zed-editor
rmpc
inputs.zen-browser.packages.${pkgs.system}.beta
starfetch
lib.custom.nixos-stable.kiwix
@ -252,6 +251,14 @@
ardour
];
programs.thunderbird = {
enable = true;
profiles.${config.home.username} = {
isDefault = true;
};
};
programs.vesktop = {
enable = true;
};

View file

@ -17,7 +17,15 @@ in {
enable = true;
settings = {
font-family = fonts.mono;
font-family = "Berkeley Mono";
font-size = 24;
font-style = "Retina ExtraCondensed";
font-style-bold = "Bold ExtraCondensed";
font-style-italic = "Retina ExtraCondensed Oblique";
font-style-bold-italic = "Bold ExtraCondensed Oblique";
gtk-single-instance = true;
gtk-titlebar = false;

View file

@ -16,15 +16,11 @@ in {
config = mkIf cfg.enable {
programs.kitty = {
enable = true;
font = {
name = "${fonts.mono} SemiLight";
size = 16;
};
extraConfig = ''
bold_font ${fonts.mono} SemiBold
italic_font ${fonts.mono} SemiLight
bold_italic_font ${fonts.mono} SemiBold
font_family family='Berkeley Mono' style='Regular ExtraCondensed'
bold_font family='Berkeley Mono' style='Retina ExtraCondensed'
italic_font family='Berkeley Mono' style='Regular ExtraCondensed'
bold_italic_font family='Berkeley Mono' style='Retina ExtraCondensed'
'';
catppuccin.enable = true;

View file

@ -0,0 +1,6 @@
return {
"chomosuke/typst-preview.nvim",
lazy = false, -- or ft = 'typst'
version = "1.*",
opts = {}, -- lazy.nvim will implicitly calls `setup {}`
}

View file

@ -0,0 +1,261 @@
{
lib,
config,
pkgs,
...
}:
with lib;
with lib.custom; let
cfg = config.apps.web.floorp;
natsumi = pkgs.fetchFromGitHub {
owner = "greeeen-dev";
repo = "natsumi-browser";
rev = "c99affebd8c095c5416e248605557c32636c6476";
hash = "sha256-5x/XLMEUb9go9Qh2E9E9UR+tSwAfR4SlvWHKq+S9A7E=";
};
in {
options.apps.web.floorp = with types; {
enable = mkBoolOpt false "Enable Floorp Browser";
setDefault = mkBoolOpt false "Enable Floorp as Default browser";
};
config = mkIf cfg.enable {
xdg.mimeApps.defaultApplications = mkIf cfg.setDefault {
"text/html" = "floorp.desktop";
"x-scheme-handler/http" = "floorp.desktop";
"x-scheme-handler/https" = "floorp.desktop";
};
home.file.".floorp/default/chrome" = {
source = "${natsumi}";
recursive = true;
};
catppuccin.firefox.profiles.default.enable = false;
programs.floorp = {
enable = true;
profiles.default = {
extensions.force = true;
search = {
default = "SearXNG";
engines = {
"NixOS Options" = {
urls = [
{
template = "https://search.nixos.org/options?query={searchTerms}";
}
];
icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg";
definedAliases = ["@nixos"];
};
"Nix Packages" = {
urls = [
{
template = "https://search.nixos.org/packages?query={searchTerms}";
}
];
icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg";
definedAliases = ["@nixpkgs"];
};
"OpenStreetMap" = {
urls = [
{
template = "https://www.openstreetmap.org/search?query={searchTerms}";
}
];
icon = "https://www.openstreetmap.org/favicon.ico";
definedAliases = ["@openstreetmap" "@osm"];
};
"SearXNG" = {
urls = [
{
template = "https://search.zoeys.computer/searx/search?q={searchTerms}";
}
];
icon = "https://search.zoeys.computer/searx/static/themes/simple/img/favicon.svg";
definedAliases = ["@searx"];
};
"docs.rs" = {
urls = [
{
template = "https://docs.rs/releases/search?query={searchTerms}";
}
];
icon = "https://docs.rs/-/static/favicon.ico";
definedAliases = ["@docs"];
};
"crates.io" = {
urls = [
{
template = "https://crates.io/search?q={searchTerms}";
}
];
icon = "https://crates.io/assets/cargo.png";
definedAliases = ["@crates"];
};
};
force = true; # Required to prevent search engine symlink being overwritten. See https://github.com/nix-community/home-manager/issues/3698
};
};
policies = {
DisableTelemetry = true;
Preferences = {
"app.normandy.api_url" = "";
"app.normandy.enabled" = false;
"app.shield.optoutstudies.enabled" = false;
"app.update.auto" = false;
"beacon.enabled" = false;
"breakpad.reportURL" = "";
"browser.aboutConfig.showWarning" = false;
"browser.cache.offline.enable" = false;
"browser.crashReports.unsubmittedCheck.autoSubmit" = false;
"browser.crashReports.unsubmittedCheck.autoSubmit2" = false;
"browser.crashReports.unsubmittedCheck.enabled" = false;
"browser.disableResetPrompt" = true;
"browser.newtab.preload" = false;
"browser.newtabpage.activity-stream.section.highlights.includePocket" = false;
"browser.newtabpage.enhanced" = false;
"browser.newtabpage.introShown" = true;
"browser.newtabpage.activity-stream.showSponsoredTopSites" = false;
"browser.newtabpage.activity-stream.showSponsored" = false;
"browser.newtabpage.activity-stream.system.showSponsored" = false;
"browser.safebrowsing.appRepURL" = "";
"browser.safebrowsing.blockedURIs.enabled" = false;
"browser.safebrowsing.downloads.enabled" = false;
"browser.safebrowsing.downloads.remote.enabled" = false;
"browser.safebrowsing.downloads.remote.url" = "";
"browser.safebrowsing.enabled" = false;
"browser.safebrowsing.malware.enabled" = false;
"browser.safebrowsing.phishing.enabled" = false;
"browser.selfsupport.url" = "";
"browser.send_pings" = false;
"browser.sessionstore.privacy_level" = 0;
"browser.shell.checkDefaultBrowser" = false;
"browser.startup.homepage_override.mstone" = "";
"browser.tabs.crashReporting.sendReport" = false;
"browser.urlbar.groupLabels.enabled" = false;
"browser.urlbar.quicksuggest.enabled" = false;
"browser.urlbar.speculativeConnect.enabled" = false;
"browser.urlbar.trimURLs" = false;
"browser.urlbar.suggest.quicksuggest.sponsored" = false;
"datareporting.healthreport.service.enabled" = false;
"datareporting.healthreport.uploadEnabled" = false;
"datareporting.policy.dataSubmissionEnabled" = false;
"device.sensors.ambientLight.enabled" = false;
"device.sensors.enabled" = false;
"device.sensors.motion.enabled" = false;
"device.sensors.orientation.enabled" = false;
"device.sensors.proximity.enabled" = false;
"dom.battery.enabled" = false;
"dom.event.clipboardevents.enabled" = false;
"dom.webaudio.enabled" = false;
"experiments.activeExperiment" = false;
"experiments.enabled" = false;
"experiments.manifest.uri" = "";
"experiments.supported" = false;
"extensions.ClearURLs@kevinr.whiteList" = "";
"extensions.Decentraleyes@ThomasRientjes.whiteList" = "";
"extensions.FirefoxMulti-AccountContainers@mozilla.whiteList" = "";
"extensions.TemporaryContainers@stoically.whiteList" = "";
"extensions.autoDisableScopes" = 14;
"extensions.getAddons.cache.enabled" = false;
"extensions.getAddons.showPane" = false;
"extensions.greasemonkey.stats.optedin" = false;
"extensions.greasemonkey.stats.url" = "";
"extensions.pocket.enabled" = false;
"extensions.shield-recipe-client.api_url" = "";
"extensions.shield-recipe-client.enabled" = false;
"extensions.webservice.discoverURL" = "";
"media.autoplay.default" = 0;
"media.autoplay.enabled" = true;
"media.eme.enabled" = false;
"media.gmp-widevinecdm.enabled" = false;
"media.navigator.enabled" = false;
"media.peerconnection.enabled" = false;
"media.video_stats.enabled" = false;
"network.IDN_show_punycode" = true;
"network.allow-experiments" = false;
"network.captive-portal-service.enabled" = false;
"network.cookie.cookieBehavior" = 1;
"network.dns.disablePrefetch" = true;
"network.dns.disablePrefetchFromHTTPS" = true;
"network.http.referer.spoofSource" = true;
"network.http.speculative-parallel-limit" = "";
"network.predictor.enable-prefetch" = false;
"network.predictor.enabled" = false;
"network.prefetch-next" = false;
"network.trr.mode" = "";
"privacy.donottrackheader.enabled" = true;
"privacy.donottrackheader.value" = "";
"privacy.firstparty.isolate" = true;
"privacy.query_stripping" = true;
"privacy.trackingprotection.cryptomining.enabled" = true;
"privacy.trackingprotection.enabled" = true;
"privacy.trackingprotection.fingerprinting.enabled" = true;
"privacy.trackingprotection.pbmode.enabled" = true;
"privacy.usercontext.about_newtab_segregation.enabled" = true;
"security.ssl.disable_session_identifiers" = true;
"services.sync.prefs.sync.browser.newtabpage.activity-stream.showSponsoredTopSite" = false;
"signon.autofillForms" = false;
"toolkit.telemetry.archive.enabled" = false;
"toolkit.telemetry.bhrPing.enabled" = false;
"toolkit.telemetry.cachedClientID" = "";
"toolkit.telemetry.enabled" = false;
"toolkit.telemetry.firstShutdownPing.enabled" = false;
"toolkit.telemetry.hybridContent.enabled" = false;
"toolkit.telemetry.newProfilePing.enabled" = false;
"toolkit.telemetry.prompted" = "";
"toolkit.telemetry.rejected" = true;
"toolkit.telemetry.reportingpolicy.firstRun" = false;
"toolkit.telemetry.server" = "";
"toolkit.telemetry.shutdownPingSender.enabled" = false;
"toolkit.telemetry.unified" = false;
"toolkit.telemetry.unifiedIsOptIn" = false;
"toolkit.telemetry.updatePing.enabled" = false;
"webgl.renderer-string-override" = " ";
"webgl.vendor-string-override" = " ";
};
ExtensionSettings = with builtins; let
extension = shortId: uuid: {
name = uuid;
value = {
install_url = "https://addons.mozilla.org/en-US/firefox/downloads/latest/${shortId}/latest.xpi";
installation_mode = "normal_installed";
};
};
in
listToAttrs [
(extension "ublock-origin" "uBlock0@raymondhill.net")
(extension "privacy-badger17" "jid1-MnnxcxisBPnSXQ@jetpack")
(extension "1password-x-password-manager" "{d634138d-c276-4fc8-924b-40a0ea21d284}")
(extension "multi-account-containers" "@testpilot-containers")
(extension "temporary-containers" "{c607c8df-14a7-4f28-894f-29e8722976af}")
(extension "styl-us" "{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}")
(extension "betterttv" "firefox@betterttv.net")
(extension "decentraleyes" "jid1-BoFifL9Vbdl2zQ@jetpack")
(extension "canvasblocker" "CanvasBlocker@kkapsner.de")
(extension "clearurls" "{74145f27-f039-47ce-a470-a662b129930a}")
];
};
};
};
}

View file

@ -2,11 +2,19 @@
lib,
config,
pkgs,
inputs,
...
}:
with lib;
with lib.custom; let
cfg = config.apps.web.librewolf;
natsumi = pkgs.fetchFromGitHub {
owner = "greeeen-dev";
repo = "natsumi-browser";
rev = "61e614d31f74ac9b17b8b5337b8870cd8f88ca96";
hash = "sha256-4xE8kg8j30vnN54InXTQVXcWLLhBqchhZpNeqiOJYLc=";
};
in {
options.apps.web.librewolf = with types; {
enable = mkBoolOpt false "Enable or disable librewolf";
@ -21,92 +29,27 @@ in {
"x-scheme-handler/https" = "librewolf.desktop";
};
home.file.".librewolf/${config.home.username}/chrome" = {
source = "${natsumi}";
recursive = true;
};
catppuccin.firefox.profiles.${config.home.username}.enable = false;
catppuccin.firefox.profiles.default.enable = false;
programs.librewolf = {
enable = true;
profiles.default = lib.mkForce {
id = 1;
isDefault = false;
};
profiles.${config.home.username} = {
id = 0;
isDefault = true;
search = {
default = "SearXNG";
engines = {
"NixOS Options" = {
urls = [
{
template = "https://search.nixos.org/options?query={searchTerms}";
}
];
icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg";
definedAliases = ["@nixos"];
};
"Nix Packages" = {
urls = [
{
template = "https://search.nixos.org/packages?query={searchTerms}";
}
];
icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg";
definedAliases = ["@nixpkgs"];
};
"OpenStreetMap" = {
urls = [
{
template = "https://www.openstreetmap.org/search?query={searchTerms}";
}
];
icon = "https://www.openstreetmap.org/favicon.ico";
definedAliases = ["@openstreetmap" "@osm"];
};
"SearXNG" = {
urls = [
{
template = "https://search.zoeys.computer/searx/search?q={searchTerms}";
}
];
icon = "https://search.zoeys.computer/searx/static/themes/simple/img/favicon.svg";
definedAliases = ["@searx"];
};
"docs.rs" = {
urls = [
{
template = "https://docs.rs/releases/search?query={searchTerms}";
}
];
icon = "https://docs.rs/-/static/favicon.ico";
definedAliases = ["@docs"];
};
"crates.io" = {
urls = [
{
template = "https://crates.io/search?q={searchTerms}";
}
];
icon = "https://crates.io/assets/cargo.png";
definedAliases = ["@crates"];
};
};
force = true; # Required to prevent search engine symlink being overwritten. See https://github.com/nix-community/home-manager/issues/3698
};
};
policies = {
DisableTelemetry = true;
Preferences = {
settings = {
"app.normandy.api_url" = "";
"app.normandy.enabled" = false;
"app.shield.optoutstudies.enabled" = false;
@ -198,7 +141,6 @@ in {
"privacy.query_stripping" = true;
"privacy.trackingprotection.cryptomining.enabled" = true;
"privacy.trackingprotection.enabled" = true;
"privacy.trackingprotection.fingerprinting.enabled" = true;
"privacy.trackingprotection.pbmode.enabled" = true;
"privacy.usercontext.about_newtab_segregation.enabled" = true;
"security.ssl.disable_session_identifiers" = true;
@ -221,8 +163,89 @@ in {
"toolkit.telemetry.updatePing.enabled" = false;
"webgl.renderer-string-override" = " ";
"webgl.vendor-string-override" = " ";
"toolkit.legacyUserProfileCustomizations.stylesheets" = true;
"sidebar.verticalTabs" = true;
"natsumi.theme.type" = "default";
};
search = {
default = "SearXNG";
engines = {
"NixOS Options" = {
urls = [
{
template = "https://search.nixos.org/options?query={searchTerms}";
}
];
icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg";
definedAliases = ["@nixos"];
};
"Nix Packages" = {
urls = [
{
template = "https://search.nixos.org/packages?query={searchTerms}";
}
];
icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg";
definedAliases = ["@nixpkgs"];
};
"OpenStreetMap" = {
urls = [
{
template = "https://www.openstreetmap.org/search?query={searchTerms}";
}
];
icon = "https://www.openstreetmap.org/favicon.ico";
definedAliases = ["@openstreetmap" "@osm"];
};
"SearXNG" = {
urls = [
{
template = "https://search.zoeys.computer/searx/search?q={searchTerms}";
}
];
icon = "https://search.zoeys.computer/searx/static/themes/simple/img/favicon.svg";
definedAliases = ["@searx"];
};
"docs.rs" = {
urls = [
{
template = "https://docs.rs/releases/search?query={searchTerms}";
}
];
icon = "https://docs.rs/-/static/favicon.ico";
definedAliases = ["@docs"];
};
"crates.io" = {
urls = [
{
template = "https://crates.io/search?q={searchTerms}";
}
];
icon = "https://crates.io/assets/cargo.png";
definedAliases = ["@crates"];
};
};
force = true; # Required to prevent search engine symlink being overwritten. See https://github.com/nix-community/home-manager/issues/3698
};
};
policies = {
ExtensionSettings = with builtins; let
extension = shortId: uuid: {
name = uuid;
@ -236,7 +259,6 @@ in {
(extension "ublock-origin" "uBlock0@raymondhill.net")
(extension "privacy-badger17" "jid1-MnnxcxisBPnSXQ@jetpack")
(extension "1password-x-password-manager" "{d634138d-c276-4fc8-924b-40a0ea21d284}")
(extension "firefox-color" "FirefoxColor@mozilla.com")
(extension "multi-account-containers" "@testpilot-containers")
(extension "temporary-containers" "{c607c8df-14a7-4f28-894f-29e8722976af}")
(extension "styl-us" "{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}")
@ -244,7 +266,6 @@ in {
(extension "decentraleyes" "jid1-BoFifL9Vbdl2zQ@jetpack")
(extension "canvasblocker" "CanvasBlocker@kkapsner.de")
(extension "clearurls" "{74145f27-f039-47ce-a470-a662b129930a}")
(extension "mtab" "contact@maxhu.dev")
];
};
};

View file

@ -16,9 +16,9 @@ in {
config = mkIf (cfg.enable && !pkgs.stdenv.isDarwin) {
xdg.mimeApps.defaultApplications = mkIf cfg.setDefault {
"text/html" = "zen-beta.desktop";
"x-scheme-handler/http" = "zen-beta.desktop";
"x-scheme-handler/https" = "zen-beta.desktop";
"text/html" = "zen-twilight.desktop";
"x-scheme-handler/http" = "zen-twilight.desktop";
"x-scheme-handler/https" = "zen-twilight.desktop";
};
};
}

View file

@ -20,6 +20,11 @@ in {
enable = mkBoolOpt false "Enable or Disable QuickShell";
};
config = mkIf cfg.enable {
xdg.configFile."quickshell" = {
source = ./qml;
recursive = true;
};
systemd.user.services = {
quickshell = mkService {
Unit.Description = "QuickShell Service";

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View file

@ -0,0 +1,80 @@
import QtQuick
import QtQuick.Shapes
import "root:/Data" as Settings
// Concave corner shape component for rounded panel edges
Shape {
id: root
property string position: "topleft" // Corner position: topleft/topright/bottomleft/bottomright
property real size: 1.0 // Scale multiplier for entire corner
property int concaveWidth: 100 * size
property int concaveHeight: 60 * size
property int offsetX: -20
property int offsetY: -20
property color fillColor: Settings.Colors.bgColor
property int arcRadius: 20 * size
// Position flags derived from position string
property bool _isTop: position.includes("top")
property bool _isLeft: position.includes("left")
property bool _isRight: position.includes("right")
property bool _isBottom: position.includes("bottom")
// Base coordinates for left corner shape
property real _baseStartX: 30 * size
property real _baseStartY: _isTop ? 20 * size : 0
property real _baseLineX: 30 * size
property real _baseLineY: _isTop ? 0 : 20 * size
property real _baseArcX: 50 * size
property real _baseArcY: _isTop ? 20 * size : 0
// Mirror coordinates for right corners
property real _startX: _isRight ? (concaveWidth - _baseStartX) : _baseStartX
property real _startY: _baseStartY
property real _lineX: _isRight ? (concaveWidth - _baseLineX) : _baseLineX
property real _lineY: _baseLineY
property real _arcX: _isRight ? (concaveWidth - _baseArcX) : _baseArcX
property real _arcY: _baseArcY
// Arc direction varies by corner to maintain proper concave shape
property int _arcDirection: {
if (_isTop && _isLeft) return PathArc.Counterclockwise
if (_isTop && _isRight) return PathArc.Clockwise
if (_isBottom && _isLeft) return PathArc.Clockwise
if (_isBottom && _isRight) return PathArc.Counterclockwise
return PathArc.Counterclockwise
}
width: concaveWidth
height: concaveHeight
// Position relative to parent based on corner type
x: _isLeft ? offsetX : (parent ? parent.width - width + offsetX : 0)
y: _isTop ? offsetY : (parent ? parent.height - height + offsetY : 0)
preferredRendererType: Shape.CurveRenderer
layer.enabled: true
layer.samples: 4
ShapePath {
strokeWidth: 0
fillColor: root.fillColor
strokeColor: root.fillColor // Use same color as fill to eliminate artifacts
startX: root._startX
startY: root._startY
PathLine {
x: root._lineX
y: root._lineY
}
PathArc {
x: root._arcX
y: root._arcY
radiusX: root.arcRadius
radiusY: root.arcRadius
useLargeArc: false
direction: root._arcDirection
}
}
}

View file

@ -0,0 +1,48 @@
import QtQuick
QtObject {
id: root
// Keep track of loaded components
property var activeLoaders: ({})
// Dynamically load a QML component
function load(componentUrl, parent, properties) {
if (!activeLoaders[componentUrl]) {
var loader = Qt.createQmlObject(`
import QtQuick
Loader {
active: false
asynchronous: true
visible: false
}
`, parent);
loader.source = componentUrl
loader.active = true
if (properties) {
for (var prop in properties) {
loader[prop] = properties[prop]
}
}
activeLoaders[componentUrl] = loader
}
return activeLoaders[componentUrl]
}
// Destroy and remove a loaded component
function unload(componentUrl) {
if (activeLoaders[componentUrl]) {
activeLoaders[componentUrl].active = false
activeLoaders[componentUrl].destroy()
delete activeLoaders[componentUrl]
}
}
// Check if a component is loaded
function isLoaded(componentUrl) {
return !!activeLoaders[componentUrl]
}
}

View file

@ -0,0 +1,175 @@
pragma Singleton
import QtQuick
import Quickshell.Io
// System process and resource monitoring
QtObject {
id: root
// System resource metrics
property real cpuUsage: 0
property real ramUsage: 0
property real totalRam: 0
property real usedRam: 0
// System control processes
property Process shutdownProcess: Process {
command: ["shutdown", "-h", "now"]
}
property Process rebootProcess: Process {
command: ["reboot"]
}
property Process lockProcess: Process {
command: ["hyprlock"]
}
property Process logoutProcess: Process {
command: ["loginctl", "terminate-user", "$USER"]
}
property Process pavucontrolProcess: Process {
command: ["pavucontrol"]
}
// Resource monitoring processes
property Process cpuProcess: Process {
command: ["sh", "-c", "grep '^cpu ' /proc/stat | awk '{usage=($2+$3+$4)*100/($2+$3+$4+$5)} END {print usage}'"]
stdout: SplitParser {
onRead: data => {
root.cpuUsage = parseFloat(data)
}
}
}
property Process ramProcess: Process {
command: ["sh", "-c", "free -b | awk '/Mem:/ {print $2\" \"$3\" \"$3/$2*100}'"]
stdout: SplitParser {
onRead: data => {
var parts = data.trim().split(/\s+/)
if (parts.length >= 3) {
root.totalRam = parseFloat(parts[0]) / (1024 * 1024 * 1024)
root.usedRam = parseFloat(parts[1]) / (1024 * 1024 * 1024)
root.ramUsage = parseFloat(parts[2])
}
}
}
}
// Monitoring timers (start manually when needed)
property Timer cpuTimer: Timer {
interval: 30000
running: false
repeat: true
onTriggered: {
cpuProcess.running = false
cpuProcess.running = true
}
}
property Timer ramTimer: Timer {
interval: 30000
running: false
repeat: true
onTriggered: {
ramProcess.running = false
ramProcess.running = true
}
}
// System control functions
function shutdown() {
console.log("Executing shutdown command")
shutdownProcess.running = true
}
function reboot() {
console.log("Executing reboot command")
rebootProcess.running = true
}
function lock() {
console.log("Executing lock command")
lockProcess.running = true
}
function logout() {
console.log("Executing logout command")
logoutProcess.running = true
}
function openPavuControl() {
console.log("Opening PavuControl")
pavucontrolProcess.running = true
}
// Performance monitoring control
function startMonitoring() {
console.log("Starting system monitoring")
cpuTimer.running = true
ramTimer.running = true
}
function stopMonitoring() {
console.log("Stopping system monitoring")
cpuTimer.running = false
ramTimer.running = false
}
function setMonitoringInterval(intervalMs) {
console.log("Setting monitoring interval to", intervalMs, "ms")
cpuTimer.interval = intervalMs
ramTimer.interval = intervalMs
}
function refreshSystemStats() {
console.log("Manually refreshing system stats")
cpuProcess.running = false
cpuProcess.running = true
ramProcess.running = false
ramProcess.running = true
}
// Process state queries
function isShutdownRunning() { return shutdownProcess.running }
function isRebootRunning() { return rebootProcess.running }
function isLockRunning() { return lockProcess.running }
function isLogoutRunning() { return logoutProcess.running }
function isPavuControlRunning() { return pavucontrolProcess.running }
function isMonitoringActive() { return cpuTimer.running && ramTimer.running }
function stopPavuControl() {
pavucontrolProcess.running = false
}
// Formatted output helpers
function getCpuUsageFormatted() {
return Math.round(cpuUsage) + "%"
}
function getRamUsageFormatted() {
return Math.round(ramUsage) + "% (" + usedRam.toFixed(1) + "GB/" + totalRam.toFixed(1) + "GB)"
}
function getRamUsageSimple() {
return Math.round(ramUsage) + "%"
}
Component.onDestruction: {
// Stop all timers
cpuTimer.running = false
ramTimer.running = false
// Stop monitoring processes
cpuProcess.running = false
ramProcess.running = false
// Stop control processes if running
if (shutdownProcess.running) shutdownProcess.running = false
if (rebootProcess.running) rebootProcess.running = false
if (lockProcess.running) lockProcess.running = false
if (logoutProcess.running) logoutProcess.running = false
if (pavucontrolProcess.running) pavucontrolProcess.running = false
}
}

View file

@ -0,0 +1,265 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
import "root:/Data" as Data
// System version watermark display
PanelWindow {
id: systemVersion
anchors {
right: true
bottom: true
}
margins {
right: 60
bottom: 60
}
visible: false
implicitWidth: systemInfoContent.width
implicitHeight: systemInfoContent.height
color: "transparent"
mask: Region {}
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.exclusiveZone: 0
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.namespace: "quickshell-version"
Timer {
id: startupTimer
interval: 1500
running: true
onTriggered: {
visible = true
}
}
component Details: QtObject {
property string version
property string commit
}
property QtObject os: QtObject {
property string name: "Loading..."
property Details details: Details {
property string generation: "?"
}
}
property QtObject wm: QtObject {
property string name: "Loading..."
property Details details: Details {}
}
Component.onCompleted: {
osFile.reload();
genProcess.running = true;
wmProcess.running = true;
niriProcess.running = true;
}
// Periodic refresh disabled - version info rarely changes
Timer {
running: false
interval: 300000
repeat: true
onTriggered: {
osFile.reload();
genProcess.running = true;
wmProcess.running = true;
niriProcess.running = true;
}
}
// Parse OS info from /etc/os-release
FileView {
id: osFile
path: "/etc/os-release"
onLoaded: {
const data = text().trim().split("\n");
const nameLine = data.find((str) => str.match(/^NAME=/));
const versionLine = data.find((str) => str.match(/^VERSION_ID=/));
const buildLine = data.find((str) => str.match(/^BUILD_ID=/));
if (nameLine) {
systemVersion.os.name = nameLine.split("=")[1].replace(/"/g, "");
}
if (versionLine) {
systemVersion.os.details.version = versionLine.split("=")[1].replace(/"/g, "");
}
if (buildLine) {
const commit = buildLine.split("=")[1].split(".")[3];
if (commit) {
systemVersion.os.details.commit = commit.replace(/"/g, "").toUpperCase();
}
}
}
}
// Get NixOS generation number
Process {
id: genProcess
running: true
command: ["sh", "-c", "nixos-rebuild list-generations"]
stdout: SplitParser {
splitMarker: ""
onRead: (data) => {
const line = data.trim().split("\n").find((str) => str.match(/current/));
if (line) {
const current = line.split(" ")[0];
systemVersion.os.details.generation = current;
}
}
}
}
// Detect desktop environment
Process {
id: wmProcess
running: true
command: ["sh", "-c", "echo $XDG_CURRENT_DESKTOP"]
stdout: SplitParser {
splitMarker: ""
onRead: (data) => {
const result = data.trim();
if (result && result !== "") {
systemVersion.wm.name = result;
}
}
}
}
// Get Niri compositor version
Process {
id: niriProcess
running: true
command: ["sh", "-c", "niri msg version"]
stdout: SplitParser {
splitMarker: ""
onRead: (data) => {
const output = data.trim();
const compositorMatch = output.match(/Compositor version: (\S+)/);
if (compositorMatch && compositorMatch[1]) {
systemVersion.wm.details.version = compositorMatch[1];
}
const commitMatch = output.match(/\((\S+)\)/);
if (commitMatch && commitMatch[1]) {
systemVersion.wm.details.commit = commitMatch[1].toUpperCase();
}
}
}
}
// macOS-inspired typography layout
ColumnLayout {
id: systemInfoContent
spacing: 6
RowLayout {
spacing: 16
Layout.alignment: Qt.AlignRight
// OS information
ColumnLayout {
spacing: 2
Layout.alignment: Qt.AlignRight
Text {
text: systemVersion.os.name
color: (Data.ThemeManager.currentTheme && Data.ThemeManager.currentTheme.type === "dark") ? "#40ffffff" : "#40000000"
font.family: "SF Pro Display, -apple-system, system-ui, sans-serif"
font.pointSize: 16
font.weight: Font.DemiBold
font.letterSpacing: -0.4
Layout.alignment: Qt.AlignRight
}
Text {
text: {
let details = [];
if (systemVersion.os.details.version) {
details.push(systemVersion.os.details.version);
}
if (systemVersion.os.details.commit) {
details.push("(" + systemVersion.os.details.commit + ")");
}
if (systemVersion.os.details.generation && systemVersion.os.details.generation !== "?") {
details.push("Gen " + systemVersion.os.details.generation);
}
return details.join(" ");
}
color: (Data.ThemeManager.currentTheme && Data.ThemeManager.currentTheme.type === "dark") ? "#30ffffff" : "#30000000"
font.family: "SF Mono, Consolas, Monaco, monospace"
font.pointSize: 10
font.weight: Font.Medium
visible: text.length > 0
Layout.alignment: Qt.AlignRight
}
}
Text {
text: "│"
color: (Data.ThemeManager.currentTheme && Data.ThemeManager.currentTheme.type === "dark") ? "#20ffffff" : "#20000000"
font.family: "SF Pro Display, -apple-system, system-ui, sans-serif"
font.pointSize: 14
font.weight: Font.Light
Layout.alignment: Qt.AlignCenter
}
// Window manager information
ColumnLayout {
spacing: 2
Layout.alignment: Qt.AlignRight
Text {
text: systemVersion.wm.name
color: (Data.ThemeManager.currentTheme && Data.ThemeManager.currentTheme.type === "dark") ? "#40ffffff" : "#40000000"
font.family: "SF Pro Display, -apple-system, system-ui, sans-serif"
font.pointSize: 16
font.weight: Font.DemiBold
font.letterSpacing: -0.4
Layout.alignment: Qt.AlignRight
}
Text {
text: {
let details = [];
if (systemVersion.wm.details.version) {
details.push(systemVersion.wm.details.version);
}
if (systemVersion.wm.details.commit) {
details.push("(" + systemVersion.wm.details.commit + ")");
}
return details.join(" ");
}
color: (Data.ThemeManager.currentTheme && Data.ThemeManager.currentTheme.type === "dark") ? "#30ffffff" : "#30000000"
font.family: "SF Mono, Consolas, Monaco, monospace"
font.pointSize: 10
font.weight: Font.Medium
visible: text.length > 0
Layout.alignment: Qt.AlignRight
}
}
}
}
Component.onDestruction: {
if (genProcess.running) genProcess.running = false
if (wmProcess.running) wmProcess.running = false
if (niriProcess.running) niriProcess.running = false
}
}

View file

@ -0,0 +1,47 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "root:/Data" as Data
// Wallpaper background layer
PanelWindow {
id: wallpaperWindow
required property var screen
anchors {
top: true
left: true
right: true
bottom: true
}
margins.top: 0
margins.left: 0
margins.right: 0
margins.bottom: 0
exclusiveZone: 0
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.namespace: "quickshell-wallpaper"
color: "transparent"
visible: true
Image {
id: wallpaperImage
anchors.fill: parent
source: Data.WallpaperManager.currentWallpaper
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false // Reduce memory usage
visible: true
// Fallback when wallpaper fails to load
Rectangle {
id: fallbackBackground
anchors.fill: parent
color: Data.ThemeManager.bgColor
visible: wallpaperImage.status !== Image.Ready || !wallpaperImage.source
}
}
}

View file

@ -0,0 +1,38 @@
pragma Singleton
import QtQuick
QtObject {
property var service: null
// Expose current colors from the service
readonly property color primary: service?.colors?.raw?.primary || "#7ed7b8"
readonly property color on_primary: service?.colors?.raw?.on_primary || "#00382a"
readonly property color primary_container: service?.colors?.raw?.primary_container || "#454b03"
readonly property color on_primary_container: service?.colors?.raw?.on_primary_container || "#e2e993"
readonly property color secondary: service?.colors?.raw?.secondary || "#c8c9a6"
readonly property color surface_bright: service?.colors?.raw?.surface_bright || "#373b30"
readonly property bool hasColors: service?.isLoaded || false
// Expose all raw Material 3 colors for complete access
readonly property var rawColors: service?.colors?.raw || ({})
function setService(matugenService) {
service = matugenService
console.log("MatugenManager: Service registered")
}
function reloadColors() {
if (service && service.reloadColors) {
console.log("MatugenManager: Triggering color reload")
service.reloadColors()
return true
} else {
console.warn("MatugenManager: No service available for reload")
return false
}
}
function isAvailable() {
return service !== null
}
}

View file

@ -0,0 +1,333 @@
pragma Singleton
import Quickshell
import QtQuick
import Quickshell.Io
Singleton {
id: settings
// Prevent auto-saving during initial load
property bool isLoading: true
// Settings persistence with atomic writes
FileView {
id: settingsFile
path: "settings.json"
blockWrites: true
atomicWrites: true
watchChanges: false
onLoaded: {
settings.isLoading = true // Disable auto-save during loading
try {
var content = JSON.parse(text())
if (content) {
// Load with fallback defaults
settings.isDarkTheme = content.isDarkTheme ?? true
settings.currentTheme = content.currentTheme ?? (content.isDarkTheme !== false ? "oxocarbon_dark" : "oxocarbon_light")
settings.useCustomAccent = content.useCustomAccent ?? false
settings.avatarSource = content.avatarSource ?? "https://cdn.discordapp.com/avatars/158005126638993408/de403b05fd7f74bb17e01a9b066a30fa?size=64"
settings.weatherLocation = content.weatherLocation ?? "Dinslaken"
settings.useFahrenheit = content.useFahrenheit ?? false
settings.displayTime = content.displayTime ?? 6000
settings.videoPath = content.videoPath ?? "~/Videos/"
settings.wallpaperDirectory = content.wallpaperDirectory ?? "/home/lysec/nixos/assets/wallpapers/"
settings.lastWallpaperPath = content.lastWallpaperPath ?? ""
settings.customDarkAccent = content.customDarkAccent ?? "#be95ff"
settings.customLightAccent = content.customLightAccent ?? "#8a3ffc"
settings.autoSwitchPlayer = content.autoSwitchPlayer ?? true
settings.alwaysShowPlayerDropdown = content.alwaysShowPlayerDropdown ?? true
settings.historyLimit = content.historyLimit ?? 25
settings.nightLightEnabled = content.nightLightEnabled ?? false
settings.nightLightWarmth = content.nightLightWarmth ?? 0.4
settings.nightLightAuto = content.nightLightAuto ?? false
settings.nightLightStartHour = content.nightLightStartHour ?? 20
settings.nightLightEndHour = content.nightLightEndHour ?? 6
settings.nightLightManualOverride = content.nightLightManualOverride ?? false
settings.nightLightManuallyEnabled = content.nightLightManuallyEnabled ?? false
settings.ignoredApps = content.ignoredApps ?? []
settings.workspaceBurstEnabled = content.workspaceBurstEnabled ?? true
settings.workspaceGlowEnabled = content.workspaceGlowEnabled ?? true
}
} catch (e) {
console.log("Error parsing user settings:", e)
}
// Re-enable auto-save after loading is complete
settings.isLoading = false
}
}
// User-configurable settings
property string avatarSource: "https://cdn.discordapp.com/avatars/158005126638993408/de403b05fd7f74bb17e01a9b066a30fa?size=64"
property bool isDarkTheme: true // Keep for backwards compatibility
property string currentTheme: "oxocarbon_dark" // New theme system
property bool useCustomAccent: false // Whether to use custom accent colors
property string weatherLocation: "Dinslaken"
property bool useFahrenheit: false // Temperature unit setting
property int displayTime: 6000 // Notification display time in ms
property var ignoredApps: [] // Apps to ignore notifications from (case-insensitive)
property int historyLimit: 25 // Notification history limit
property string videoPath: "~/Videos/"
property string wallpaperDirectory: "/home/lysec/nixos/assets/wallpapers/"
property string lastWallpaperPath: ""
property string customDarkAccent: "#be95ff"
property string customLightAccent: "#8a3ffc"
// Music Player settings
property bool autoSwitchPlayer: true
property bool alwaysShowPlayerDropdown: true
// Night Light settings
property bool nightLightEnabled: false
property real nightLightWarmth: 0.4
property bool nightLightAuto: false
property int nightLightStartHour: 20 // 8 PM
property int nightLightEndHour: 6 // 6 AM
property bool nightLightManualOverride: false // Track manual user actions
property bool nightLightManuallyEnabled: false // Track if user manually enabled it
// Animation settings
property bool workspaceBurstEnabled: true
property bool workspaceGlowEnabled: true
// UI constants
readonly property real borderWidth: 9
readonly property real cornerRadius: 20
signal settingsChanged()
// Helper functions for managing ignored apps
function addIgnoredApp(appName) {
if (appName && appName.trim() !== "") {
var trimmedName = appName.trim()
// Case-insensitive check for existing apps
var exists = false
for (var i = 0; i < ignoredApps.length; i++) {
if (ignoredApps[i].toLowerCase() === trimmedName.toLowerCase()) {
exists = true
break
}
}
if (!exists) {
var newApps = ignoredApps.slice() // Create a copy
newApps.push(trimmedName)
ignoredApps = newApps
console.log("Added ignored app:", trimmedName, "Current list:", ignoredApps)
// Force save immediately (only if not loading)
if (!isLoading) {
saveSettings()
}
return true
}
}
return false
}
function removeIgnoredApp(appName) {
var index = ignoredApps.indexOf(appName)
if (index > -1) {
var newApps = ignoredApps.slice() // Create a copy
newApps.splice(index, 1)
ignoredApps = newApps
console.log("Removed ignored app:", appName, "Current list:", ignoredApps)
// Force save immediately (only if not loading)
if (!isLoading) {
saveSettings()
}
return true
}
return false
}
function saveSettings() {
try {
var content = {
isDarkTheme: settings.isDarkTheme,
currentTheme: settings.currentTheme,
useCustomAccent: settings.useCustomAccent,
avatarSource: settings.avatarSource,
weatherLocation: settings.weatherLocation,
useFahrenheit: settings.useFahrenheit,
displayTime: settings.displayTime,
videoPath: settings.videoPath,
wallpaperDirectory: settings.wallpaperDirectory,
lastWallpaperPath: settings.lastWallpaperPath,
customDarkAccent: settings.customDarkAccent,
customLightAccent: settings.customLightAccent,
autoSwitchPlayer: settings.autoSwitchPlayer,
alwaysShowPlayerDropdown: settings.alwaysShowPlayerDropdown,
historyLimit: settings.historyLimit,
nightLightEnabled: settings.nightLightEnabled,
nightLightWarmth: settings.nightLightWarmth,
nightLightAuto: settings.nightLightAuto,
nightLightStartHour: settings.nightLightStartHour,
nightLightEndHour: settings.nightLightEndHour,
nightLightManualOverride: settings.nightLightManualOverride,
nightLightManuallyEnabled: settings.nightLightManuallyEnabled,
ignoredApps: settings.ignoredApps,
workspaceBurstEnabled: settings.workspaceBurstEnabled,
workspaceGlowEnabled: settings.workspaceGlowEnabled
}
var jsonContent = JSON.stringify(content, null, 4)
settingsFile.setText(jsonContent)
} catch (e) {
console.log("Error saving user settings:", e)
}
}
// Auto-save watchers (only save when not loading)
onIsDarkThemeChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onCurrentThemeChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onUseCustomAccentChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onAvatarSourceChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onWeatherLocationChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onUseFahrenheitChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onDisplayTimeChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onHistoryLimitChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onVideoPathChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onWallpaperDirectoryChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onLastWallpaperPathChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onCustomDarkAccentChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onCustomLightAccentChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onAutoSwitchPlayerChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onAlwaysShowPlayerDropdownChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onNightLightEnabledChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onNightLightWarmthChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onNightLightAutoChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onNightLightStartHourChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onNightLightEndHourChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onNightLightManualOverrideChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onNightLightManuallyEnabledChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onIgnoredAppsChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onWorkspaceBurstEnabledChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
onWorkspaceGlowEnabledChanged: {
if (!isLoading) {
settingsChanged()
saveSettings()
}
}
Component.onCompleted: {
settingsFile.reload()
}
}

View file

@ -0,0 +1,243 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import "Themes" as Themes
Singleton {
id: themeManager
// Import all theme definitions
property var oxocarbon: Themes.Oxocarbon
property var dracula: Themes.Dracula
property var gruvbox: Themes.Gruvbox
property var catppuccin: Themes.Catppuccin
property var matugen: Themes.Matugen
// Available theme definitions
readonly property var themes: ({
"oxocarbon_dark": oxocarbon.dark,
"oxocarbon_light": oxocarbon.light,
"dracula_dark": dracula.dark,
"dracula_light": dracula.light,
"gruvbox_dark": gruvbox.dark,
"gruvbox_light": gruvbox.light,
"catppuccin_dark": catppuccin.dark,
"catppuccin_light": catppuccin.light,
"matugen_dark": matugen.dark,
"matugen_light": matugen.light
})
// Current theme selection - defaults to oxocarbon_dark if not set
readonly property string currentThemeId: Settings.currentTheme || "oxocarbon_dark"
readonly property var currentTheme: themes[currentThemeId] || themes["oxocarbon_dark"]
// Auto-update accents when Matugen colors change
Connections {
target: MatugenManager
function onPrimaryChanged() {
if (currentThemeId.startsWith("matugen_")) {
updateMatugenAccents()
}
}
}
// Connect to MatugenService signals for automatic accent updates
Connections {
target: MatugenManager.service
function onMatugenColorsUpdated() {
if (currentThemeId.startsWith("matugen_")) {
console.log("ThemeManager: Received matugen colors update signal")
updateMatugenAccents()
}
}
}
// Initialize currentTheme in settings if not present
Component.onCompleted: {
if (!Settings.currentTheme) {
console.log("Initializing currentTheme in settings")
Settings.currentTheme = "oxocarbon_dark"
Settings.saveSettings()
}
// Matugen theme is now self-contained with service-based colors
console.log("Matugen theme initialized with service-based colors")
// Update accents if already using matugen theme
if (currentThemeId.startsWith("matugen_")) {
updateMatugenAccents()
}
}
// Custom accent colors (can be changed by user)
property string customDarkAccent: Settings.customDarkAccent || "#be95ff"
property string customLightAccent: Settings.customLightAccent || "#8a3ffc"
// Dynamic color properties based on current theme
readonly property color base00: currentTheme.base00
readonly property color base01: currentTheme.base01
readonly property color base02: currentTheme.base02
readonly property color base03: currentTheme.base03
readonly property color base04: currentTheme.base04
readonly property color base05: currentTheme.base05
readonly property color base06: currentTheme.base06
readonly property color base07: currentTheme.base07
readonly property color base08: currentTheme.base08
readonly property color base09: currentTheme.base09
readonly property color base0A: currentTheme.base0A
readonly property color base0B: currentTheme.base0B
readonly property color base0C: currentTheme.base0C
readonly property color base0D: currentTheme.base0D
readonly property color base0E: Settings.useCustomAccent ?
(currentTheme.type === "dark" ? customDarkAccent : customLightAccent) : currentTheme.base0E
readonly property color base0F: currentTheme.base0F
// Common UI color mappings
readonly property color bgColor: base00
readonly property color bgLight: base01
readonly property color bgLighter: base02
readonly property color fgColor: base04
readonly property color fgColorBright: base05
readonly property color accentColor: base0E
readonly property color accentColorBright: base0D
readonly property color highlightBg: Qt.rgba(base0E.r, base0E.g, base0E.b, 0.15)
readonly property color errorColor: base08
readonly property color greenColor: base0B
readonly property color redColor: base08
// Alternative semantic aliases for convenience
readonly property color background: base00
readonly property color panelBackground: base01
readonly property color selection: base02
readonly property color border: base03
readonly property color secondaryText: base04
readonly property color primaryText: base05
readonly property color brightText: base06
readonly property color brightestText: base07
readonly property color error: base08
readonly property color warning: base09
readonly property color highlight: base0A
readonly property color success: base0B
readonly property color info: base0C
readonly property color primary: base0D
readonly property color accent: base0E
readonly property color special: base0F
// UI styling constants
readonly property real borderWidth: 9
readonly property real cornerRadius: 20
// Color utility functions
function withOpacity(color, opacity) {
return Qt.rgba(color.r, color.g, color.b, opacity)
}
function withHighlight(color) {
return Qt.rgba(color.r, color.g, color.b, 0.15)
}
// Theme management functions
function setTheme(themeId) {
if (themes[themeId]) {
const previousThemeId = Settings.currentTheme
Settings.currentTheme = themeId
// Check if switching between matugen light/dark modes
if (themeId.startsWith("matugen_") && previousThemeId && previousThemeId.startsWith("matugen_")) {
const newMode = themeId.includes("_light") ? "light" : "dark"
const oldMode = previousThemeId.includes("_light") ? "light" : "dark"
if (newMode !== oldMode) {
console.log(`🎨 Switching matugen from ${oldMode} to ${newMode} mode`)
WallpaperManager.regenerateColorsForMode(newMode)
}
}
// Auto-update accents for Matugen themes
if (themeId.startsWith("matugen_")) {
updateMatugenAccents()
}
Settings.saveSettings()
return true
}
return false
}
// Auto-update accent colors when using Matugen theme
function updateMatugenAccents() {
if (MatugenManager.isAvailable() && MatugenManager.hasColors) {
// Get colors from the raw matugen palette
const rawColors = MatugenManager.rawColors
// Use primary for both dark and light themes - it's generated appropriately by matugen
const accent = rawColors.primary
// Debug log the colors we're using
console.log("Raw colors available:", Object.keys(rawColors))
console.log("Selected accent for both themes:", accent)
// Update custom accents - use the same accent for both
setCustomAccent(accent, accent)
// Enable custom accents for Matugen theme
Settings.useCustomAccent = true
Settings.saveSettings()
console.log("Auto-updated Matugen accents from service:", accent)
} else {
console.log("MatugenManager service not available or no colors loaded yet")
}
}
function getThemeList() {
return Object.keys(themes).map(function(key) {
return {
id: key,
name: themes[key].name,
type: themes[key].type
}
})
}
function getDarkThemes() {
return getThemeList().filter(function(theme) {
return theme.type === "dark"
})
}
function getLightThemes() {
return getThemeList().filter(function(theme) {
return theme.type === "light"
})
}
function setCustomAccent(darkColor, lightColor) {
customDarkAccent = darkColor
customLightAccent = lightColor
Settings.customDarkAccent = darkColor
Settings.customLightAccent = lightColor
Settings.saveSettings()
}
function toggleCustomAccent() {
Settings.useCustomAccent = !Settings.useCustomAccent
Settings.saveSettings()
}
// Legacy function for backwards compatibility
function toggleTheme() {
// Switch between dark and light variants of current theme family
var currentFamily = currentThemeId.replace(/_dark|_light/, "")
var newThemeId = currentTheme.type === "dark" ?
currentFamily + "_light" : currentFamily + "_dark"
// If the opposite variant doesn't exist, switch to oxocarbon
if (!themes[newThemeId]) {
newThemeId = currentTheme.type === "dark" ? "oxocarbon_light" : "oxocarbon_dark"
}
setTheme(newThemeId)
}
}

View file

@ -0,0 +1,46 @@
pragma Singleton
import QtQuick
QtObject {
readonly property var dark: ({
name: "Catppuccin Mocha",
type: "dark",
base00: "#1e1e2e", // Base
base01: "#181825", // Mantle
base02: "#313244", // Surface0
base03: "#45475a", // Surface1
base04: "#585b70", // Surface2
base05: "#cdd6f4", // Text
base06: "#f5e0dc", // Rosewater
base07: "#b4befe", // Lavender
base08: "#f38ba8", // Red
base09: "#fab387", // Peach
base0A: "#f9e2af", // Yellow
base0B: "#a6e3a1", // Green
base0C: "#94e2d5", // Teal
base0D: "#89b4fa", // Blue
base0E: "#cba6f7", // Mauve
base0F: "#f2cdcd" // Flamingo
})
readonly property var light: ({
name: "Catppuccin Latte",
type: "light",
base00: "#eff1f5", // Base
base01: "#e6e9ef", // Mantle
base02: "#ccd0da", // Surface0
base03: "#bcc0cc", // Surface1
base04: "#acb0be", // Surface2
base05: "#4c4f69", // Text
base06: "#dc8a78", // Rosewater
base07: "#7287fd", // Lavender
base08: "#d20f39", // Red
base09: "#fe640b", // Peach
base0A: "#df8e1d", // Yellow
base0B: "#40a02b", // Green
base0C: "#179299", // Teal
base0D: "#1e66f5", // Blue
base0E: "#8839ef", // Mauve
base0F: "#dd7878" // Flamingo
})
}

View file

@ -0,0 +1,46 @@
pragma Singleton
import QtQuick
QtObject {
readonly property var dark: ({
name: "Dracula",
type: "dark",
base00: "#282a36", // Background
base01: "#44475a", // Current line
base02: "#565761", // Selection
base03: "#6272a4", // Comment
base04: "#6272a4", // Dark foreground
base05: "#f8f8f2", // Foreground
base06: "#f8f8f2", // Light foreground
base07: "#ffffff", // Light background
base08: "#ff5555", // Red
base09: "#ffb86c", // Orange
base0A: "#f1fa8c", // Yellow
base0B: "#50fa7b", // Green
base0C: "#8be9fd", // Cyan
base0D: "#bd93f9", // Blue
base0E: "#ff79c6", // Magenta
base0F: "#ffb86c" // Orange
})
readonly property var light: ({
name: "Dracula Light",
type: "light",
base00: "#f8f8f2", // Light background
base01: "#ffffff", // Lighter background
base02: "#e5e5e5", // Selection
base03: "#bfbfbf", // Comment
base04: "#6272a4", // Dark foreground
base05: "#282a36", // Dark text
base06: "#21222c", // Darker text
base07: "#191a21", // Darkest
base08: "#e74c3c", // Red (adjusted for light)
base09: "#f39c12", // Orange
base0A: "#f1c40f", // Yellow
base0B: "#27ae60", // Green
base0C: "#17a2b8", // Cyan
base0D: "#6c7ce0", // Blue
base0E: "#e91e63", // Magenta
base0F: "#f39c12" // Orange
})
}

View file

@ -0,0 +1,46 @@
pragma Singleton
import QtQuick
QtObject {
readonly property var dark: ({
name: "Gruvbox Dark",
type: "dark",
base00: "#282828", // Dark background
base01: "#3c3836", // Dark1
base02: "#504945", // Dark2
base03: "#665c54", // Dark3
base04: "#bdae93", // Light4
base05: "#d5c4a1", // Light3
base06: "#ebdbb2", // Light2
base07: "#fbf1c7", // Light1
base08: "#fb4934", // Red
base09: "#fe8019", // Orange
base0A: "#fabd2f", // Yellow
base0B: "#b8bb26", // Green
base0C: "#8ec07c", // Cyan
base0D: "#83a598", // Blue
base0E: "#d3869b", // Purple
base0F: "#d65d0e" // Brown
})
readonly property var light: ({
name: "Gruvbox Light",
type: "light",
base00: "#fbf1c7", // Light background
base01: "#ebdbb2", // Light1
base02: "#d5c4a1", // Light2
base03: "#bdae93", // Light3
base04: "#665c54", // Dark3
base05: "#504945", // Dark2
base06: "#3c3836", // Dark1
base07: "#282828", // Dark background
base08: "#cc241d", // Red
base09: "#d65d0e", // Orange
base0A: "#d79921", // Yellow
base0B: "#98971a", // Green
base0C: "#689d6a", // Cyan
base0D: "#458588", // Blue
base0E: "#b16286", // Purple
base0F: "#d65d0e" // Brown
})
}

View file

@ -0,0 +1,143 @@
pragma Singleton
import QtQuick
QtObject {
// Reference to the MatugenService
property var matugenService: null
// Debug helper to check service status
function debugServiceStatus() {
console.log("🔍 Debug: matugenService =", matugenService)
console.log("🔍 Debug: matugenService.isLoaded =", matugenService ? matugenService.isLoaded : "N/A")
console.log("🔍 Debug: matugenService.colorVersion =", matugenService ? matugenService.colorVersion : "N/A")
console.log("🔍 Debug: condition result =", (matugenService && matugenService.isLoaded && matugenService.colorVersion >= 0))
if (matugenService && matugenService.colors) {
console.log("🔍 Debug: service.colors.dark =", JSON.stringify(matugenService.colors.dark))
}
}
// Map matugen colors to base16 scheme - using the service when available
// The colorVersion dependency forces re-evaluation when colors update
readonly property var dark: {
debugServiceStatus()
if (matugenService && matugenService.isLoaded && matugenService.colorVersion >= 0) {
// Use service colors if available, or generate fallback if we have light colors
return matugenService.colors.dark || {
name: "Matugen Dark (Generated from Light)",
type: "dark",
// If we only have light colors, create dark fallback
base00: "#141311",
base01: "#1c1c19",
base02: "#20201d",
base03: "#2a2a27",
base04: "#c9c7ba",
base05: "#e5e2de",
base06: "#31302e",
base07: "#e5e2de",
base08: "#ffb4ab",
base09: "#b5ccb9",
base0A: "#e4e5c1",
base0B: "#c8c7b7",
base0C: "#c8c9a6",
base0D: "#c8c9a6",
base0E: "#47483b",
base0F: "#000000"
}
} else {
return {
name: "Matugen Dark",
type: "dark",
// Updated fallback colors to match current quickshell-colors.qml
base00: "#141311",
base01: "#1c1c19",
base02: "#20201d",
base03: "#2a2a27",
base04: "#c9c7ba",
base05: "#e5e2de",
base06: "#31302e",
base07: "#e5e2de",
base08: "#ffb4ab",
base09: "#b5ccb9",
base0A: "#e4e5c1",
base0B: "#c8c7b7",
base0C: "#c8c9a6",
base0D: "#c8c9a6",
base0E: "#47483b",
base0F: "#000000"
}
}
}
readonly property var light: {
if (matugenService && matugenService.isLoaded && matugenService.colorVersion >= 0) {
// Use service colors if available, or generate fallback if we have dark colors
return matugenService.colors.light || {
name: "Matugen Light (Generated from Dark)",
type: "light",
// If we only have dark colors, create light fallback
base00: "#ffffff",
base01: "#f5f5f5",
base02: "#e8e8e8",
base03: "#d0d0d0",
base04: "#666666",
base05: "#1a1a1a",
base06: "#000000",
base07: "#ffffff",
base08: "#d32f2f",
base09: "#7b1fa2",
base0A: "#f57c00",
base0B: "#388e3c",
base0C: "#0097a7",
base0D: "#1976d2",
base0E: "#5e35b1",
base0F: "#000000"
}
} else {
return {
name: "Matugen Light",
type: "light",
// Updated fallback colors based on current colors
base00: "#ffffff",
base01: "#f5f5f5",
base02: "#e8e8e8",
base03: "#d0d0d0",
base04: "#666666",
base05: "#1a1a1a",
base06: "#000000",
base07: "#ffffff",
base08: "#d32f2f",
base09: "#7b1fa2",
base0A: "#f57c00",
base0B: "#388e3c",
base0C: "#0097a7",
base0D: "#1976d2",
base0E: "#5e35b1",
base0F: "#000000"
}
}
}
// Direct access to primary colors for accent updates
readonly property color primary: (matugenService && matugenService.getColor && matugenService.colorVersion >= 0) ?
matugenService.getColor("primary") || "#c8c9a6" : "#c8c9a6"
readonly property color on_primary: (matugenService && matugenService.getColor && matugenService.colorVersion >= 0) ?
matugenService.getColor("on_primary") || "#303219" : "#303219"
// Function to set the service reference
function setMatugenService(service) {
matugenService = service
console.log("🔌 MatugenService connected to theme:", service)
// Connect to service signals for automatic updates
if (service) {
service.matugenColorsUpdated.connect(function() {
console.log("🎨 Matugen colors updated in theme (version " + service.colorVersion + ")")
debugServiceStatus()
})
}
}
Component.onCompleted: {
console.log("Matugen theme loaded, waiting for MatugenService connection")
}
}

View file

@ -0,0 +1,46 @@
pragma Singleton
import QtQuick
QtObject {
readonly property var dark: ({
name: "Oxocarbon Dark",
type: "dark",
base00: "#161616", // OLED-friendly background
base01: "#262626", // Surface 1
base02: "#393939", // Surface 2
base03: "#525252", // Surface 3
base04: "#6f6f6f", // Text secondary
base05: "#c6c6c6", // Text primary
base06: "#e0e0e0", // Text on color
base07: "#f4f4f4", // Text inverse
base08: "#ff7eb6", // Red (pink)
base09: "#ee5396", // Magenta
base0A: "#42be65", // Green
base0B: "#be95ff", // Purple
base0C: "#3ddbd9", // Cyan
base0D: "#78a9ff", // Blue
base0E: "#be95ff", // Purple (accent)
base0F: "#08bdba" // Teal
})
readonly property var light: ({
name: "Oxocarbon Light",
type: "light",
base00: "#f4f4f4", // Light background
base01: "#ffffff", // Surface 1
base02: "#e0e0e0", // Surface 2
base03: "#c6c6c6", // Surface 3
base04: "#525252", // Text secondary
base05: "#262626", // Text primary
base06: "#161616", // Text on color
base07: "#000000", // Text inverse
base08: "#da1e28", // Red
base09: "#d12771", // Magenta
base0A: "#198038", // Green
base0B: "#8a3ffc", // Purple
base0C: "#007d79", // Cyan
base0D: "#0f62fe", // Blue
base0E: "#8a3ffc", // Purple (accent)
base0F: "#005d5d" // Teal
})
}

View file

@ -0,0 +1,170 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import "." as Data
// Wallpaper manager with auto-scan
Item {
id: manager
property string wallpaperDirectory: Data.Settings.wallpaperDirectory
property string currentWallpaper: Data.Settings.lastWallpaperPath
property var wallpaperList: []
// Watch for wallpaper directory changes and refresh
Connections {
target: Data.Settings
function onWallpaperDirectoryChanged() {
console.log("Wallpaper directory changed to:", Data.Settings.wallpaperDirectory)
wallpaperDirectory = Data.Settings.wallpaperDirectory
wallpaperList = [] // Clear current list
loadWallpapers() // Scan new directory
}
}
// Auto-refresh (5 min)
Timer {
id: refreshTimer
interval: 300000
running: false
repeat: true
onTriggered: loadWallpapers()
}
// Scan directory for wallpapers
Process {
id: findProcessInternal
property var callback
property var tempList: []
running: false
command: ["find", manager.wallpaperDirectory, "-type", "f", "-name", "*.png", "-o", "-name", "*.jpg", "-o", "-name", "*.jpeg"]
// Note: WebP excluded as Qt WebP support requires additional plugins not always available
stdout: SplitParser {
splitMarker: "\n"
onRead: (line) => {
if (line.trim()) {
findProcessInternal.tempList.push(line.trim())
}
}
}
onExited: {
var newList = findProcessInternal.tempList.slice()
manager.wallpaperList = newList
findProcessInternal.tempList = []
// Set first wallpaper if none selected
if (!currentWallpaper && wallpaperList.length > 0) {
setWallpaper(wallpaperList[0])
}
// Start refresh timer after first successful scan
if (!refreshTimer.running) {
refreshTimer.running = true
}
if (callback) callback()
}
}
function loadWallpapers(cb) {
findProcessInternal.callback = cb
findProcessInternal.tempList = []
findProcessInternal.running = true
}
function setWallpaper(path) {
currentWallpaper = path
Data.Settings.lastWallpaperPath = path
// Detect current theme mode for matugen
const currentTheme = Data.Settings.currentTheme || "oxocarbon_dark"
const mode = currentTheme.includes("_light") ? "light" : "dark"
// Generate matugen colors from the new wallpaper with appropriate mode
generateMatugenColors(path, mode)
// Trigger update across all wallpaper components
currentWallpaperChanged()
}
// Process for running matugen
Process {
id: matugenProcess
running: false
onExited: {
if (exitCode === 0) {
console.log("✓ Matugen colors generated successfully")
// Trigger MatugenService reload through the manager
Qt.callLater(function() {
if (Data.MatugenManager.reloadColors()) {
console.log("🔄 MatugenService reload triggered successfully")
} else {
console.warn("⚠️ Could not trigger MatugenService reload")
}
})
} else {
console.warn("✗ Matugen failed with exit code:", exitCode)
}
running = false
}
onStarted: {
console.log("🎨 Generating matugen colors for wallpaper...")
}
}
// Generate colors using matugen
function generateMatugenColors(wallpaperPath, mode) {
if (!wallpaperPath) return
// Default to dark mode if not specified
const themeMode = mode || "dark"
const modeFlag = themeMode === "light" ? "-m light" : ""
// Run matugen to generate colors for quickshell
matugenProcess.command = [
"sh", "-c",
`matugen image "${wallpaperPath}" ${modeFlag} && echo "Matugen completed for ${themeMode} mode"`
]
matugenProcess.running = true
}
// Regenerate colors for current wallpaper with different mode
function regenerateColorsForMode(mode) {
if (currentWallpaper) {
console.log(`🎨 Regenerating matugen colors for ${mode} mode...`)
generateMatugenColors(currentWallpaper, mode)
} else {
console.warn("No current wallpaper set, cannot regenerate colors")
}
}
// Ensure wallpapers are loaded before executing callback
function ensureWallpapersLoaded(callback) {
if (wallpaperList.length === 0) {
loadWallpapers(callback)
} else if (callback) {
callback()
}
}
Component.onCompleted: {
if (Data.Settings.lastWallpaperPath) {
currentWallpaper = Data.Settings.lastWallpaperPath
}
}
Component.onDestruction: {
if (findProcessInternal.running) {
findProcessInternal.running = false
}
if (refreshTimer.running) {
refreshTimer.running = false
}
}
}

View file

@ -0,0 +1,60 @@
pragma Singleton
import Quickshell
import QtQuick
Singleton {
readonly property color background: "#13140c"
readonly property color error: "#ffb4ab"
readonly property color error_container: "#93000a"
readonly property color inverse_on_surface: "#313128"
readonly property color inverse_primary: "#5d631c"
readonly property color inverse_surface: "#e5e3d6"
readonly property color on_background: "#e5e3d6"
readonly property color on_error: "#690005"
readonly property color on_error_container: "#ffdad6"
readonly property color on_primary: "#2f3300"
readonly property color on_primary_container: "#e2e993"
readonly property color on_primary_fixed: "#1b1d00"
readonly property color on_primary_fixed_variant: "#454b03"
readonly property color on_secondary: "#30321a"
readonly property color on_secondary_container: "#e4e5c1"
readonly property color on_secondary_fixed: "#1b1d07"
readonly property color on_secondary_fixed_variant: "#47492e"
readonly property color on_surface: "#e5e3d6"
readonly property color on_surface_variant: "#c8c7b7"
readonly property color on_tertiary: "#07372c"
readonly property color on_tertiary_container: "#beecdc"
readonly property color on_tertiary_fixed: "#002019"
readonly property color on_tertiary_fixed_variant: "#234e42"
readonly property color outline: "#929182"
readonly property color outline_variant: "#47483b"
readonly property color primary: "#c5cc7a"
readonly property color primary_container: "#454b03"
readonly property color primary_fixed: "#e2e993"
readonly property color primary_fixed_dim: "#c5cc7a"
readonly property color scrim: "#000000"
readonly property color secondary: "#c8c9a6"
readonly property color secondary_container: "#47492e"
readonly property color secondary_fixed: "#e4e5c1"
readonly property color secondary_fixed_dim: "#c8c9a6"
readonly property color shadow: "#000000"
readonly property color surface: "#13140c"
readonly property color surface_bright: "#3a3a31"
readonly property color surface_container: "#202018"
readonly property color surface_container_high: "#2a2a22"
readonly property color surface_container_highest: "#35352c"
readonly property color surface_container_low: "#1c1c14"
readonly property color surface_container_lowest: "#0e0f08"
readonly property color surface_dim: "#13140c"
readonly property color surface_tint: "#c5cc7a"
readonly property color surface_variant: "#47483b"
readonly property color tertiary: "#a3d0c0"
readonly property color tertiary_container: "#234e42"
readonly property color tertiary_fixed: "#beecdc"
readonly property color tertiary_fixed_dim: "#a3d0c0"
function withAlpha(color: color, alpha: real): color {
return Qt.rgba(color.r, color.g, color.b, alpha)
}
}

View file

@ -0,0 +1,9 @@
{
"isDarkTheme": true,
"avatarSource": "https://cdn.discordapp.com/avatars/158005126638993408/de403b05fd7f74bb17e01a9b066a30fa?size=64",
"weatherLocation": "Dinslaken",
"displayTime": 6000,
"videoPath": "~/Videos/",
"wallpaperDirectory": "/home/lysec/nixos/assets/wallpapers/",
"lastWallpaperPath": "/home/lysec/nixos/assets/wallpapers/girl.jpg"
}

View file

@ -0,0 +1,35 @@
import QtQuick
import QtQuick.Effects
import "root:/Data" as Data
import "root:/Widgets/System" as System
import "root:/Widgets/Calendar" as Calendar
// Vertical sidebar layout
Rectangle {
id: bar
// Clean bar background
color: Data.ThemeManager.bgColor
// Workspace indicator at top
System.NiriWorkspaces {
id: workspaceIndicator
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
horizontalCenterOffset: Data.Settings.borderWidth / 2
topMargin: 20
}
}
// Clock at bottom
Calendar.Clock {
id: clockWidget
anchors {
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
horizontalCenterOffset: Data.Settings.borderWidth / 2
bottomMargin: 20
}
}
}

View file

@ -0,0 +1,575 @@
import QtQuick
import QtQuick.Shapes
import Qt5Compat.GraphicalEffects
import QtQuick.Effects
import "root:/Data" as Data
// Screen border with shadow effects
Shape {
id: borderShape
// Border dimensions
property real borderWidth: Data.Settings.borderWidth
property real radius: Data.Settings.cornerRadius
property real innerX: borderWidth
property real innerY: borderWidth
property real innerWidth: borderShape.width - (borderWidth * 2)
property real innerHeight: borderShape.height - (borderWidth * 2)
// Widget references for shadow positioning
property var workspaceIndicator: null
property var volumeOSD: null
property var clockWidget: null
// Initialization state to prevent ShaderEffect warnings
property bool effectsReady: false
// Burst effect properties - controlled by workspace indicator
property real masterProgress: workspaceIndicator ? workspaceIndicator.masterProgress : 0.0
property bool effectsActive: workspaceIndicator ? workspaceIndicator.effectsActive : false
property color effectColor: workspaceIndicator ? workspaceIndicator.effectColor : Data.ThemeManager.accent
// Delay graphics effects until component is fully loaded
Timer {
id: initTimer
interval: 100
running: true
onTriggered: borderShape.effectsReady = true
}
// Burst effect overlays (DISABLED - using unified overlay)
Item {
id: burstEffects
anchors.fill: parent
visible: false // Disabled in favor of unified overlay
z: 5
}
// Individual widget shadows (positioned separately)
// Workspace indicator shadow
Shape {
id: workspaceDropShadow
visible: borderShape.workspaceIndicator !== null
x: borderShape.workspaceIndicator ? borderShape.workspaceIndicator.x : 0 // Exact match
y: borderShape.workspaceIndicator ? borderShape.workspaceIndicator.y : 0
width: borderShape.workspaceIndicator ? borderShape.workspaceIndicator.width : 0 // Exact match
height: borderShape.workspaceIndicator ? borderShape.workspaceIndicator.height : 0
z: -1
layer.enabled: borderShape.workspaceIndicator !== null
layer.effect: DropShadow {
transparentBorder: true
horizontalOffset: 1
verticalOffset: 1
radius: 12 + (effectsActive && Data.Settings.workspaceGlowEnabled ? Math.sin(masterProgress * Math.PI) * 4 : 0)
samples: 25
color: {
if (!effectsActive)
return Qt.rgba(0, 0, 0, 0.4);
if (!Data.Settings.workspaceGlowEnabled)
return Qt.rgba(0, 0, 0, 0.4);
// Use accent color directly with reduced intensity
const intensity = Math.sin(masterProgress * Math.PI) * 0.4;
return Qt.rgba(effectColor.r * intensity + 0.08, effectColor.g * intensity + 0.08, effectColor.b * intensity + 0.08, 0.4 + intensity * 0.2);
}
cached: true
spread: 0.2 + (effectsActive && Data.Settings.workspaceGlowEnabled ? Math.sin(masterProgress * Math.PI) * 0.15 : 0)
}
ShapePath {
strokeWidth: 0
fillColor: "black"
startX: 12
startY: 0
// Right side - standard rounded corners
PathLine {
x: workspaceDropShadow.width - 16
y: 0
}
PathArc {
x: workspaceDropShadow.width
y: 16
radiusX: 16
radiusY: 16
direction: PathArc.Clockwise
}
PathLine {
x: workspaceDropShadow.width
y: workspaceDropShadow.height - 16
}
PathArc {
x: workspaceDropShadow.width - 16
y: workspaceDropShadow.height
radiusX: 16
radiusY: 16
direction: PathArc.Clockwise
}
PathLine {
x: 12
y: workspaceDropShadow.height
}
// Left side - concave curves for border integration
PathLine {
x: 0
y: workspaceDropShadow.height - 12
}
PathArc {
x: 12
y: workspaceDropShadow.height - 24
radiusX: 12
radiusY: 12
direction: PathArc.Clockwise
}
PathLine {
x: 12
y: 24
}
PathArc {
x: 0
y: 12
radiusX: 12
radiusY: 12
direction: PathArc.Clockwise
}
PathLine {
x: 12
y: 0
}
}
}
// Volume OSD shadow
Rectangle {
id: volumeOsdDropShadow
visible: borderShape.volumeOSD !== null && borderShape.volumeOSD.visible
opacity: borderShape.volumeOSD ? borderShape.volumeOSD.opacity : 0
x: parent.width - 45
y: (parent.height - 250) / 2
width: 45
height: 250
color: "black"
topLeftRadius: 20
bottomLeftRadius: 20
topRightRadius: 0
bottomRightRadius: 0
z: -1
// Sync opacity animations with volume OSD
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
layer.enabled: borderShape.volumeOSD !== null
layer.effect: DropShadow {
transparentBorder: true
horizontalOffset: -1
verticalOffset: 1
radius: 12 // Much more subtle
samples: 25
color: Qt.rgba(0, 0, 0, 0.4) // Very light shadow
cached: false
spread: 0.2 // Minimal spread
}
}
// Clock shadow
Rectangle {
id: clockDropShadow
visible: borderShape.clockWidget !== null
x: borderShape.clockWidget ? borderShape.clockWidget.x : 0
y: borderShape.clockWidget ? borderShape.clockWidget.y : 0
width: borderShape.clockWidget ? borderShape.clockWidget.width : 0
height: borderShape.clockWidget ? borderShape.clockWidget.height : 0
color: "black"
topLeftRadius: 0
topRightRadius: borderShape.clockWidget ? borderShape.clockWidget.height / 2 : 16
bottomLeftRadius: 0
bottomRightRadius: 0
z: -2 // Lower z-index to render behind border corners
layer.enabled: borderShape.clockWidget !== null
layer.effect: DropShadow {
transparentBorder: true
horizontalOffset: 1
verticalOffset: -1
radius: 12 // Much more subtle
samples: 25
color: Qt.rgba(0, 0, 0, 0.4) // Very light shadow
cached: false
spread: 0.2 // Minimal spread
}
}
// Shadow rendering source (hidden)
Item {
id: shadowSource
anchors.fill: parent
visible: false
Shape {
id: borderShadowShape
anchors.fill: parent
layer.enabled: true
layer.samples: 4
ShapePath {
fillColor: "black"
strokeWidth: 0
fillRule: ShapePath.OddEvenFill
// Outer rectangle (full screen)
PathMove {
x: 0
y: 0
}
PathLine {
x: shadowSource.width
y: 0
}
PathLine {
x: shadowSource.width
y: shadowSource.height
}
PathLine {
x: 0
y: shadowSource.height
}
PathLine {
x: 0
y: 0
}
// Inner rounded cutout creates border
PathMove {
x: borderShape.innerX + borderShape.radius
y: borderShape.innerY
}
PathLine {
x: borderShape.innerX + borderShape.innerWidth - borderShape.radius
y: borderShape.innerY
}
PathArc {
x: borderShape.innerX + borderShape.innerWidth
y: borderShape.innerY + borderShape.radius
radiusX: borderShape.radius
radiusY: borderShape.radius
direction: PathArc.Clockwise
}
PathLine {
x: borderShape.innerX + borderShape.innerWidth
y: borderShape.innerY + borderShape.innerHeight - borderShape.radius
}
PathArc {
x: borderShape.innerX + borderShape.innerWidth - borderShape.radius
y: borderShape.innerY + borderShape.innerHeight
radiusX: borderShape.radius
radiusY: borderShape.radius
direction: PathArc.Clockwise
}
PathLine {
x: borderShape.innerX + borderShape.radius
y: borderShape.innerY + borderShape.innerHeight
}
PathArc {
x: borderShape.innerX
y: borderShape.innerY + borderShape.innerHeight - borderShape.radius
radiusX: borderShape.radius
radiusY: borderShape.radius
direction: PathArc.Clockwise
}
PathLine {
x: borderShape.innerX
y: borderShape.innerY + borderShape.radius
}
PathArc {
x: borderShape.innerX + borderShape.radius
y: borderShape.innerY
radiusX: borderShape.radius
radiusY: borderShape.radius
direction: PathArc.Clockwise
}
}
}
// Workspace indicator shadow with concave curves
Shape {
id: workspaceShadowShape
visible: borderShape.workspaceIndicator !== null
x: borderShape.workspaceIndicator ? borderShape.workspaceIndicator.x : 0 // Exact match
y: borderShape.workspaceIndicator ? borderShape.workspaceIndicator.y : 0
width: borderShape.workspaceIndicator ? borderShape.workspaceIndicator.width : 0 // Exact match
height: borderShape.workspaceIndicator ? borderShape.workspaceIndicator.height : 0
preferredRendererType: Shape.CurveRenderer
layer.enabled: borderShape.workspaceIndicator !== null
layer.samples: 8
layer.effect: DropShadow {
transparentBorder: true
horizontalOffset: 2
verticalOffset: 3
radius: 25 + (effectsActive && Data.Settings.workspaceGlowEnabled ? Math.sin(masterProgress * Math.PI) * 6 : 0)
samples: 40
color: {
if (!effectsActive)
return Qt.rgba(0, 0, 0, 0.8);
if (!Data.Settings.workspaceGlowEnabled)
return Qt.rgba(0, 0, 0, 0.8);
// Accent color glow with reduced intensity
const intensity = Math.sin(masterProgress * Math.PI) * 0.3;
return Qt.rgba(effectColor.r * intensity + 0.1, effectColor.g * intensity + 0.1, effectColor.b * intensity + 0.1, 0.6 + intensity * 0.15);
}
cached: false
spread: 0.5 + (effectsActive && Data.Settings.workspaceGlowEnabled ? Math.sin(masterProgress * Math.PI) * 0.2 : 0)
}
ShapePath {
strokeWidth: 0
fillColor: "black"
strokeColor: "black"
startX: 12
startY: 0
// Right side - standard rounded corners
PathLine {
x: workspaceShadowShape.width - 16
y: 0
}
PathArc {
x: workspaceShadowShape.width
y: 16
radiusX: 16
radiusY: 16
direction: PathArc.Clockwise
}
PathLine {
x: workspaceShadowShape.width
y: workspaceShadowShape.height - 16
}
PathArc {
x: workspaceShadowShape.width - 16
y: workspaceShadowShape.height
radiusX: 16
radiusY: 16
direction: PathArc.Clockwise
}
PathLine {
x: 12
y: workspaceShadowShape.height
}
// Left side - concave curves for border integration
PathLine {
x: 0
y: workspaceShadowShape.height - 12
}
PathArc {
x: 12
y: workspaceShadowShape.height - 24
radiusX: 12
radiusY: 12
direction: PathArc.Clockwise
}
PathLine {
x: 12
y: 24
}
PathArc {
x: 0
y: 12
radiusX: 12
radiusY: 12
direction: PathArc.Clockwise
}
PathLine {
x: 12
y: 0
}
}
}
// Volume OSD shadow
Rectangle {
id: volumeOsdShadowShape
visible: borderShape.volumeOSD !== null && borderShape.volumeOSD.visible
x: shadowSource.width - 45
y: (shadowSource.height - 250) / 2
width: 45
height: 250
color: "black"
topLeftRadius: 20
bottomLeftRadius: 20
topRightRadius: 0
bottomRightRadius: 0
layer.enabled: borderShape.volumeOSD !== null && borderShape.volumeOSD.visible
layer.effect: DropShadow {
transparentBorder: true
horizontalOffset: -2 // Shadow to the left for right-side widget
verticalOffset: 3
radius: 25
samples: 40
color: Qt.rgba(0, 0, 0, 0.8)
cached: false
spread: 0.5
}
}
// Clock shadow
Rectangle {
id: clockShadowShape
visible: borderShape.clockWidget !== null
x: borderShape.clockWidget ? borderShape.clockWidget.x : 0
y: borderShape.clockWidget ? borderShape.clockWidget.y : 0
width: borderShape.clockWidget ? borderShape.clockWidget.width : 0
height: borderShape.clockWidget ? borderShape.clockWidget.height : 0
color: "black"
topLeftRadius: 0
topRightRadius: borderShape.clockWidget ? borderShape.clockWidget.height / 2 : 16
bottomLeftRadius: 0
bottomRightRadius: 0
layer.enabled: borderShape.clockWidget !== null
layer.effect: DropShadow {
transparentBorder: true
horizontalOffset: 2
verticalOffset: -2 // Shadow upward for bottom widget
radius: 25
samples: 40
color: Qt.rgba(0, 0, 0, 0.8)
cached: false
spread: 0.5
}
}
}
// Apply shadow effect to entire border shape
layer.enabled: true
layer.samples: 8
layer.smooth: true
layer.effect: DropShadow {
transparentBorder: true
horizontalOffset: 1
verticalOffset: 2
radius: 30 // Slightly less dramatic
samples: 45
color: Qt.rgba(0, 0, 0, 0.75) // A bit lighter
cached: false
spread: 0.5 // Less spread
}
// Main border shape
ShapePath {
fillColor: Data.ThemeManager.bgColor
strokeWidth: 0
fillRule: ShapePath.OddEvenFill
// Outer rectangle
PathMove {
x: 0
y: 0
}
PathLine {
x: borderShape.width
y: 0
}
PathLine {
x: borderShape.width
y: borderShape.height
}
PathLine {
x: 0
y: borderShape.height
}
PathLine {
x: 0
y: 0
}
// Inner rounded cutout
PathMove {
x: borderShape.innerX + borderShape.radius
y: borderShape.innerY
}
PathLine {
x: borderShape.innerX + borderShape.innerWidth - borderShape.radius
y: borderShape.innerY
}
PathArc {
x: borderShape.innerX + borderShape.innerWidth
y: borderShape.innerY + borderShape.radius
radiusX: borderShape.radius
radiusY: borderShape.radius
direction: PathArc.Clockwise
}
PathLine {
x: borderShape.innerX + borderShape.innerWidth
y: borderShape.innerY + borderShape.innerHeight - borderShape.radius
}
PathArc {
x: borderShape.innerX + borderShape.innerWidth - borderShape.radius
y: borderShape.innerY + borderShape.innerHeight
radiusX: borderShape.radius
radiusY: borderShape.radius
direction: PathArc.Clockwise
}
PathLine {
x: borderShape.innerX + borderShape.radius
y: borderShape.innerY + borderShape.innerHeight
}
PathArc {
x: borderShape.innerX
y: borderShape.innerY + borderShape.innerHeight - borderShape.radius
radiusX: borderShape.radius
radiusY: borderShape.radius
direction: PathArc.Clockwise
}
PathLine {
x: borderShape.innerX
y: borderShape.innerY + borderShape.radius
}
PathArc {
x: borderShape.innerX + borderShape.radius
y: borderShape.innerY
radiusX: borderShape.radius
radiusY: borderShape.radius
direction: PathArc.Clockwise
}
}
}

View file

@ -0,0 +1,318 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import Quickshell.Wayland
import Qt5Compat.GraphicalEffects
import "root:/Data" as Data
import "root:/Widgets/System" as System
import "root:/Core" as Core
import "root:/Widgets" as Widgets
import "root:/Widgets/Notifications" as Notifications
import "root:/Widgets/ControlPanel" as ControlPanel
// Desktop with borders and UI widgets
Scope {
id: desktop
property var shell
property var notificationService
// Wallpaper layer - one per screen
Variants {
model: Quickshell.screens
Core.Wallpaper {
required property var modelData
screen: modelData
}
}
// Desktop UI layer per screen
Variants {
model: Quickshell.screens
PanelWindow {
required property var modelData
screen: modelData
implicitWidth: Screen.width
implicitHeight: Screen.height
color: "transparent"
exclusiveZone: 0
WlrLayershell.namespace: "quickshell-desktop"
// Interactive mask for workspace indicator only
mask: Region {
item: workspaceIndicator
}
anchors {
top: true
left: true
bottom: true
right: true
}
// Workspace indicator at left border
System.NiriWorkspaces {
id: workspaceIndicator
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: Data.Settings.borderWidth
}
z: 10
width: 32
}
// Volume OSD at right border (primary screen only)
System.OSD {
id: osd
shell: desktop.shell
visible: modelData === Quickshell.primaryScreen
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
rightMargin: Data.Settings.borderWidth
}
z: 10
}
// Widget shadows (positioned behind border for proper layering)
// Workspace indicator shadow
Rectangle {
id: workspaceShadow
visible: workspaceIndicator !== null
x: workspaceIndicator.x
y: workspaceIndicator.y
width: workspaceIndicator.width
height: workspaceIndicator.height
color: "black"
radius: 16
z: -10 // Behind border
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true
horizontalOffset: 2
verticalOffset: 2
radius: 8 + (workspaceIndicator.effectsActive && Data.Settings.workspaceGlowEnabled ? Math.sin(workspaceIndicator.masterProgress * Math.PI) * 3 : 0)
samples: 17
color: {
if (!workspaceIndicator.effectsActive) return Qt.rgba(0, 0, 0, 0.3)
if (!Data.Settings.workspaceGlowEnabled) return Qt.rgba(0, 0, 0, 0.3)
// Use accent color glow with reduced intensity
const intensity = Math.sin(workspaceIndicator.masterProgress * Math.PI) * 0.3
return Qt.rgba(
workspaceIndicator.effectColor.r * intensity + 0.05,
workspaceIndicator.effectColor.g * intensity + 0.05,
workspaceIndicator.effectColor.b * intensity + 0.05,
0.3 + intensity * 0.15
)
}
cached: true
spread: 0.1 + (workspaceIndicator.effectsActive && Data.Settings.workspaceGlowEnabled ? Math.sin(workspaceIndicator.masterProgress * Math.PI) * 0.1 : 0)
}
}
// Clock widget shadow
Rectangle {
id: clockShadow
visible: clockWidget !== null
x: clockWidget.x
y: clockWidget.y
width: clockWidget.width
height: clockWidget.height
color: "black"
topLeftRadius: 0
topRightRadius: clockWidget.height / 2
bottomLeftRadius: 0
bottomRightRadius: 0
z: -10 // Behind border
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true
horizontalOffset: 1
verticalOffset: -1
radius: 8
samples: 17
color: Qt.rgba(0, 0, 0, 0.3)
cached: true
spread: 0.1
}
}
// Border background with shadow
Border {
id: screenBorder
anchors.fill: parent
workspaceIndicator: workspaceIndicator
volumeOSD: volumeOsd
clockWidget: clockWidget
z: -5 // Behind UI elements to prevent shadow from covering control panel
}
// Unified Wave Overlay - simple burst effect
Item {
id: waveOverlay
anchors.fill: parent
visible: workspaceIndicator.effectsActive && Data.Settings.workspaceBurstEnabled
z: 15
property real progress: workspaceIndicator.masterProgress
property color waveColor: workspaceIndicator.effectColor
// Workspace indicator burst effects
Item {
x: workspaceIndicator.x
y: workspaceIndicator.y
width: workspaceIndicator.width
height: workspaceIndicator.height
// Expanding pill burst - positioned at current workspace index (mimics pill shape)
Rectangle {
x: parent.width / 2 - width / 2
y: {
// Find current workspace index directly from currentWorkspace
let focusedIndex = 0
for (let i = 0; i < workspaceIndicator.workspaces.count; i++) {
const workspace = workspaceIndicator.workspaces.get(i)
if (workspace && workspace.id === workspaceIndicator.currentWorkspace) {
focusedIndex = i
break
}
}
// Calculate position accounting for Column centering and pill sizes
let cumulativeHeight = 0
for (let i = 0; i < focusedIndex; i++) {
const ws = workspaceIndicator.workspaces.get(i)
cumulativeHeight += (ws && ws.isFocused ? 36 : 22) + 6 // pill height + spacing
}
// Current pill height
const currentWs = workspaceIndicator.workspaces.get(focusedIndex)
const currentPillHeight = (currentWs && currentWs.isFocused ? 36 : 22)
// Column is centered, so start from center and calculate offset
const columnHeight = parent.height - 24 // Total available height minus padding
const columnTop = 12 // Top padding
return columnTop + cumulativeHeight + currentPillHeight / 2 - height / 2
}
width: 20 + waveOverlay.progress * 30
height: 36 + waveOverlay.progress * 20 // Pill-like height
radius: width / 2 // Pill-like rounded shape
color: "transparent"
border.width: 2
border.color: Qt.rgba(waveOverlay.waveColor.r, waveOverlay.waveColor.g, waveOverlay.waveColor.b, 1.0 - waveOverlay.progress)
opacity: Math.max(0, 1.0 - waveOverlay.progress)
}
// Secondary expanding pill burst - positioned at current workspace index
Rectangle {
x: parent.width / 2 - width / 2
y: {
// Find current workspace index directly from currentWorkspace
let focusedIndex = 0
for (let i = 0; i < workspaceIndicator.workspaces.count; i++) {
const workspace = workspaceIndicator.workspaces.get(i)
if (workspace && workspace.id === workspaceIndicator.currentWorkspace) {
focusedIndex = i
break
}
}
// Calculate position accounting for Column centering and pill sizes
let cumulativeHeight = 0
for (let i = 0; i < focusedIndex; i++) {
const ws = workspaceIndicator.workspaces.get(i)
cumulativeHeight += (ws && ws.isFocused ? 36 : 22) + 6 // pill height + spacing
}
// Current pill height
const currentWs = workspaceIndicator.workspaces.get(focusedIndex)
const currentPillHeight = (currentWs && currentWs.isFocused ? 36 : 22)
// Column is centered, so start from center and calculate offset
const columnHeight = parent.height - 24 // Total available height minus padding
const columnTop = 12 // Top padding
return columnTop + cumulativeHeight + currentPillHeight / 2 - height / 2
}
width: 18 + waveOverlay.progress * 45
height: 30 + waveOverlay.progress * 35 // Pill-like height
radius: width / 2 // Pill-like rounded shape
color: "transparent"
border.width: 1.5
border.color: Qt.rgba(waveOverlay.waveColor.r, waveOverlay.waveColor.g, waveOverlay.waveColor.b, 0.6)
opacity: Math.max(0, 0.8 - waveOverlay.progress * 1.2)
visible: waveOverlay.progress > 0.2
}
}
}
// Clock at bottom-left corner
Widgets.Clock {
id: clockWidget
anchors {
bottom: parent.bottom
left: parent.left
bottomMargin: Data.Settings.borderWidth
leftMargin: Data.Settings.borderWidth
}
z: 10
}
// Notification popups (primary screen only)
Notifications.Notification {
id: notificationPopup
visible: (modelData === (Quickshell.primaryScreen || Quickshell.screens[0])) && calculatedHeight > 20
anchors {
top: parent.top
right: parent.right
rightMargin: Data.Settings.borderWidth + 20
topMargin: 0
}
width: 420
height: calculatedHeight
shell: desktop.shell
notificationServer: desktop.notificationService ? desktop.notificationService.notificationServer : null
z: 15
Component.onCompleted: {
let targetScreen = Quickshell.primaryScreen || Quickshell.screens[0]
if (modelData === targetScreen) {
desktop.shell.notificationWindow = notificationPopup
}
}
}
// UI overlay layer for modal components
Item {
id: uiLayer
anchors.fill: parent
z: 20
Core.Version {
visible: modelData === Quickshell.primaryScreen
}
ControlPanel.ControlPanel {
id: controlPanelComponent
shell: desktop.shell
}
}
}
}
// Handle dynamic screen configuration changes
Connections {
target: Quickshell
function onScreensChanged() {
// Screen changes handled by Variants automatically
}
}
}

View file

@ -0,0 +1,415 @@
import QtQuick
import Quickshell
import Quickshell.Io
// App launcher service - discovers and manages applications
Item {
id: appService
property var applications: []
property bool isLoading: false
// Categories for apps
property var categories: {
"AudioVideo": "🎵",
"Audio": "🎵",
"Video": "🎬",
"Development": "💻",
"Education": "📚",
"Game": "🎮",
"Graphics": "🎨",
"Network": "🌐",
"Office": "📄",
"Science": "🔬",
"Settings": "⚙️",
"System": "🔧",
"Utility": "🛠️",
"Other": "📦"
}
property string userName: ""
property string homeDirectory: ""
property bool userInfoLoaded: false
property var currentApp: ({})
property var pendingSearchPaths: []
Component.onCompleted: {
// First get user info, then load applications
loadUserInfo()
}
function loadUserInfo() {
userNameProcess.running = true
}
Process {
id: userNameProcess
command: ["whoami"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: (line) => {
if (line.trim()) {
userName = line.trim()
}
}
}
onExited: {
// Now get home directory
homeDirProcess.running = true
}
}
Process {
id: homeDirProcess
command: ["sh", "-c", "echo $HOME"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: (line) => {
if (line.trim()) {
homeDirectory = line.trim()
}
}
}
onExited: {
// Now we have user info, start loading applications
userInfoLoaded = true
loadApplications()
}
}
function loadApplications() {
if (!userInfoLoaded) {
console.log("User info not loaded yet, skipping application scan")
return
}
isLoading = true
applications = []
console.log("DEBUG: Starting application scan with user:", userName, "home:", homeDirectory)
// Comprehensive search paths for maximum Linux compatibility
appService.pendingSearchPaths = [
// User-specific locations (highest priority)
homeDirectory + "/.local/share/applications/",
// Standard FreeDesktop.org locations
"/usr/share/applications/",
"/usr/local/share/applications/",
// Flatpak locations
"/var/lib/flatpak/exports/share/applications/",
homeDirectory + "/.local/share/flatpak/exports/share/applications/",
// Snap locations
"/var/lib/snapd/desktop/applications/",
"/snap/bin/",
// AppImage locations (common user directories)
homeDirectory + "/Applications/",
homeDirectory + "/AppImages/",
// Distribution-specific paths
"/opt/*/share/applications/", // For manually installed software
"/usr/share/applications/kde4/", // KDE4 legacy
// NixOS-specific (will be ignored on non-NixOS systems)
"/run/current-system/sw/share/applications/",
"/etc/profiles/per-user/" + userName + "/share/applications/"
]
console.log("DEBUG: Starting with essential paths:", JSON.stringify(appService.pendingSearchPaths))
// Add XDG and home-manager paths
getXdgDataDirs.running = true
}
Process {
id: getXdgDataDirs
command: ["sh", "-c", "echo $XDG_DATA_DIRS"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: (line) => {
if (line.trim()) {
var xdgDirs = line.trim().split(":")
for (var i = 0; i < xdgDirs.length; i++) {
if (xdgDirs[i].trim()) {
var xdgPath = xdgDirs[i].trim() + "/applications/"
if (appService.pendingSearchPaths.indexOf(xdgPath) === -1) {
appService.pendingSearchPaths.push(xdgPath)
console.log("DEBUG: Added XDG path:", xdgPath)
}
}
}
}
}
}
onExited: {
// Now add home-manager path
getHomeManagerPaths.running = true
}
}
Process {
id: getHomeManagerPaths
command: ["sh", "-c", "find /nix/store -maxdepth 1 -name '*home-manager-path*' -type d 2>/dev/null | head -1"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: (line) => {
if (line.trim()) {
var homeManagerPath = line.trim() + "/share/applications/"
appService.pendingSearchPaths.push(homeManagerPath)
console.log("DEBUG: Added home-manager path:", homeManagerPath)
}
}
}
onExited: {
// CRITICAL: Always ensure these essential directories are included
var essentialPaths = [
"/run/current-system/sw/share/applications/",
"/usr/share/applications/",
"/usr/local/share/applications/"
]
for (var i = 0; i < essentialPaths.length; i++) {
var path = essentialPaths[i]
if (appService.pendingSearchPaths.indexOf(path) === -1) {
appService.pendingSearchPaths.push(path)
console.log("DEBUG: Added missing essential path:", path)
}
}
// Start bulk parsing with all paths including XDG and home-manager
startBulkParsing(appService.pendingSearchPaths)
}
}
function startBulkParsing(searchPaths) {
// BULLETPROOF: Ensure critical system directories are always included
var criticalPaths = [
"/run/current-system/sw/share/applications/",
"/usr/share/applications/",
"/usr/local/share/applications/"
]
for (var i = 0; i < criticalPaths.length; i++) {
var path = criticalPaths[i]
if (searchPaths.indexOf(path) === -1) {
searchPaths.push(path)
console.log("DEBUG: BULLETPROOF: Added missing critical path:", path)
}
}
console.log("DEBUG: Final directories to scan:", searchPaths.join(", "))
// Single command to parse all .desktop files at once
// Only parse fields from the main [Desktop Entry] section, ignore [Desktop Action] sections
var cmd = 'for dir in ' + searchPaths.map(p => "'" + p + "'").join(" ") + '; do ' +
'if [ -d "$dir" ]; then ' +
'find "$dir" -name "*.desktop" 2>/dev/null | while read file; do ' +
'echo "===FILE:$file"; ' +
'sed -n \'/^\\[Desktop Entry\\]/,/^\\[.*\\]/{/^\\[Desktop Entry\\]/d; /^\\[.*\\]/q; /^Name=/p; /^Exec=/p; /^Icon=/p; /^Comment=/p; /^Categories=/p; /^Hidden=/p; /^NoDisplay=/p}\' "$file" 2>/dev/null || true; ' +
'done; ' +
'fi; ' +
'done'
bulkParseProcess.command = ["sh", "-c", cmd]
bulkParseProcess.running = true
}
Process {
id: bulkParseProcess
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: (line) => {
if (line.startsWith("===FILE:")) {
// Start of new file
if (appService.currentApp.name && appService.currentApp.exec && !appService.currentApp.hidden && !appService.currentApp.noDisplay) {
applications.push(appService.currentApp)
}
appService.currentApp = {
name: "",
exec: "",
icon: "",
comment: "",
categories: [],
hidden: false,
noDisplay: false,
filePath: line.substring(8) // Remove "===FILE:" prefix
}
} else if (line.startsWith("Name=")) {
appService.currentApp.name = line.substring(5)
} else if (line.startsWith("Exec=")) {
appService.currentApp.exec = line.substring(5)
} else if (line.startsWith("Icon=")) {
appService.currentApp.icon = line.substring(5)
} else if (line.startsWith("Comment=")) {
appService.currentApp.comment = line.substring(8)
} else if (line.startsWith("Categories=")) {
appService.currentApp.categories = line.substring(11).split(";").filter(cat => cat.length > 0)
} else if (line === "Hidden=true") {
appService.currentApp.hidden = true
} else if (line === "NoDisplay=true") {
appService.currentApp.noDisplay = true
}
}
}
onStarted: {
appService.currentApp = {}
}
onExited: {
// Process the last app
if (appService.currentApp.name && appService.currentApp.exec && !appService.currentApp.hidden && !appService.currentApp.noDisplay) {
applications.push(appService.currentApp)
}
console.log("DEBUG: Before deduplication: Found", applications.length, "applications")
// Deduplicate applications - prefer user installations over system ones
var uniqueApps = {}
var finalApps = []
for (var i = 0; i < applications.length; i++) {
var app = applications[i]
var key = app.name + "|" + app.exec.split(" ")[0] // Use name + base command as key
if (!uniqueApps[key]) {
// First occurrence of this app
uniqueApps[key] = app
finalApps.push(app)
} else {
// Duplicate found - check if this version should replace the existing one
var existing = uniqueApps[key]
var shouldReplace = false
// Priority order (higher priority replaces lower):
// 1. User local applications (highest priority)
// 2. Home-manager applications
// 3. User profile applications
// 4. System applications (lowest priority)
if (app.filePath.includes("/.local/share/applications/")) {
shouldReplace = true // User local always wins
} else if (app.filePath.includes("home-manager-path") &&
!existing.filePath.includes("/.local/share/applications/")) {
shouldReplace = true // Home-manager beats system
} else if (app.filePath.includes("/home/") &&
!existing.filePath.includes("/.local/share/applications/") &&
!existing.filePath.includes("home-manager-path")) {
shouldReplace = true // User profile beats system
}
if (shouldReplace) {
// Replace the existing app in finalApps array
for (var j = 0; j < finalApps.length; j++) {
if (finalApps[j] === existing) {
finalApps[j] = app
uniqueApps[key] = app
break
}
}
}
// If not replacing, just ignore the duplicate
}
}
applications = finalApps
console.log("DEBUG: After deduplication: Found", applications.length, "unique applications")
isLoading = false
applicationsChanged()
}
}
function launchApplication(app) {
if (!app || !app.exec) return
// Clean up the exec command (remove field codes like %f, %F, %u, %U)
var cleanExec = app.exec.replace(/%[fFuU]/g, "").trim()
launchProcess.command = ["sh", "-c", cleanExec]
launchProcess.running = true
console.log("Launching:", app.name, "with command:", cleanExec)
}
Process {
id: launchProcess
running: false
onExited: {
if (exitCode !== 0) {
console.log("Failed to launch application, exit code:", exitCode)
}
}
}
// Fuzzy search function
function fuzzySearch(query, apps) {
if (!query || query.length === 0) {
return apps
}
query = query.toLowerCase()
return apps.filter(app => {
var searchText = (app.name + " " + app.comment).toLowerCase()
// Simple fuzzy matching - check if all characters of query appear in order
var queryIndex = 0
for (var i = 0; i < searchText.length && queryIndex < query.length; i++) {
if (searchText[i] === query[queryIndex]) {
queryIndex++
}
}
return queryIndex === query.length
}).sort((a, b) => {
// Sort by relevance - exact matches first, then by name
var aName = a.name.toLowerCase()
var bName = b.name.toLowerCase()
var aExact = aName.includes(query)
var bExact = bName.includes(query)
if (aExact && !bExact) return -1
if (!aExact && bExact) return 1
return aName.localeCompare(bName)
})
}
function getCategoryIcon(app) {
if (!app.categories || app.categories.length === 0) {
return categories["Other"]
}
// Find the first matching category
for (var i = 0; i < app.categories.length; i++) {
var category = app.categories[i]
if (categories[category]) {
return categories[category]
}
}
return categories["Other"]
}
}

View file

@ -0,0 +1,140 @@
import QtQuick
import Quickshell.Io
import "root:/Data" as Data
// Matugen color integration service
Item {
id: service
property var shell
property var colors: ({})
property bool isLoaded: false
property int colorVersion: 0 // Increments every time colors update to force QML re-evaluation
// Signals to notify when colors change
signal matugenColorsUpdated()
signal matugenColorsLoaded()
// File watcher for the matugen quickshell-colors.qml
FileView {
id: matugenFile
path: "/home/lysec/nixos/modules/quickshell/qml/Data/quickshell-colors.qml"
blockWrites: true
onLoaded: {
parseColors(text())
}
onTextChanged: {
parseColors(text())
}
}
// Parse QML color definitions and map them to base16 colors
function parseColors(qmlText) {
if (!qmlText) {
console.warn("MatugenService: No QML content to parse")
return
}
const lines = qmlText.split('\n')
const parsedColors = {}
// Extract readonly property color definitions
for (const line of lines) {
const match = line.match(/readonly\s+property\s+color\s+(\w+):\s*"(#[0-9a-fA-F]{6})"/)
if (match) {
const colorName = match[1]
const colorValue = match[2]
parsedColors[colorName] = colorValue
}
}
// Detect if this is a light or dark theme based on surface luminance
const surfaceColor = parsedColors.surface || "#000000"
const isLightTheme = getLuminance(surfaceColor) > 0.5
console.log(`MatugenService: Detected ${isLightTheme ? 'light' : 'dark'} theme from surface color: ${surfaceColor}`)
// Use Material Design 3 colors directly with better contrast
const baseMapping = {
base00: parsedColors.surface || (isLightTheme ? "#ffffff" : "#000000"), // Background
base01: parsedColors.surface_container_low || (isLightTheme ? "#f8f9fa" : "#1a1a1a"), // Panel bg
base02: parsedColors.surface_container || (isLightTheme ? "#e9ecef" : "#2a2a2a"), // Selection
base03: parsedColors.surface_container_high || (isLightTheme ? "#dee2e6" : "#3a3a3a"), // Border/separator
base04: parsedColors.on_surface_variant || (isLightTheme ? "#6c757d" : "#adb5bd"), // Secondary text (better contrast)
base05: parsedColors.on_surface || (isLightTheme ? "#212529" : "#f8f9fa"), // Primary text (high contrast)
base06: parsedColors.on_background || (isLightTheme ? "#000000" : "#ffffff"), // Bright text
base07: isLightTheme ? parsedColors.surface_container_lowest || "#ffffff" : parsedColors.surface_bright || "#ffffff", // Brightest
base08: isLightTheme ? parsedColors.on_error || "#dc3545" : parsedColors.error || "#ff6b6b", // Error (theme appropriate)
base09: parsedColors.tertiary || (isLightTheme ? "#6f42c1" : "#a855f7"), // Purple
base0A: parsedColors.primary_fixed || (isLightTheme ? "#fd7e14" : "#fbbf24"), // Orange/Yellow
base0B: parsedColors.secondary || (isLightTheme ? "#198754" : "#10b981"), // Green
base0C: parsedColors.surface_tint || (isLightTheme ? "#0dcaf0" : "#06b6d4"), // Cyan
base0D: parsedColors.primary_container || (isLightTheme ? "#0d6efd" : "#3b82f6"), // Blue
base0E: parsedColors.primary || (isLightTheme ? "#6610f2" : parsedColors.secondary || "#8b5cf6"), // Accent - use primary for light, secondary for dark
base0F: parsedColors.scrim || "#000000" // Special/black
}
// Create the theme object
const theme = Object.assign({
name: isLightTheme ? "Matugen Light" : "Matugen Dark",
type: isLightTheme ? "light" : "dark"
}, baseMapping)
// Store colors in the appropriate theme slot
colors = {
raw: parsedColors,
[isLightTheme ? 'light' : 'dark']: theme,
// Keep the other theme as null or use fallback
[isLightTheme ? 'dark' : 'light']: null
}
isLoaded = true
colorVersion++ // Increment version to force QML property updates
console.log("MatugenService: Colors loaded successfully from QML (version " + colorVersion + ")")
console.log("Available colors:", Object.keys(parsedColors).join(", "))
// Emit signals to notify theme system
matugenColorsUpdated()
matugenColorsLoaded()
}
// Calculate luminance of a hex color
function getLuminance(hexColor) {
// Remove # if present
const hex = hexColor.replace('#', '')
// Convert to RGB
const r = parseInt(hex.substr(0, 2), 16) / 255
const g = parseInt(hex.substr(2, 2), 16) / 255
const b = parseInt(hex.substr(4, 2), 16) / 255
// Calculate relative luminance
const rs = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4)
const gs = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4)
const bs = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4)
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs
}
// Reload colors from file
function reloadColors() {
matugenFile.reload()
}
// Get specific color by name
function getColor(colorName) {
return colors.raw ? colors.raw[colorName] : null
}
// Check if matugen colors are available
function isAvailable() {
return isLoaded && colors.raw && Object.keys(colors.raw).length > 0
}
Component.onCompleted: {
console.log("MatugenService: Initialized, watching quickshell-colors.qml")
}
}

View file

@ -0,0 +1,97 @@
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
import "root:/Data/" as Data
// Notification service with app filtering
Item {
id: service
property var shell
property alias notificationServer: notificationServer
property int maxHistorySize: Data.Settings.historyLimit
property int cleanupThreshold: maxHistorySize + 10
// Periodic cleanup every 30 minutes
Timer {
interval: 1800000
running: true
repeat: true
onTriggered: cleanupNotifications()
}
function cleanupNotifications() {
if (shell.notificationHistory && shell.notificationHistory.count > cleanupThreshold) {
const removeCount = shell.notificationHistory.count - maxHistorySize
shell.notificationHistory.remove(maxHistorySize, removeCount)
}
// Remove invalid entries
if (shell.notificationHistory) {
for (let i = shell.notificationHistory.count - 1; i >= 0; i--) {
const item = shell.notificationHistory.get(i)
if (!item || !item.appName) {
shell.notificationHistory.remove(i)
}
}
}
}
NotificationServer {
id: notificationServer
actionsSupported: true
bodyMarkupSupported: true
imageSupported: true
keepOnReload: false
persistenceSupported: true
onNotification: (notification) => {
// Filter empty notifications
if (!notification.appName && !notification.summary && !notification.body) {
if (typeof notification.dismiss === 'function') {
notification.dismiss()
}
return
}
// Filter ignored applications (case-insensitive)
var shouldIgnore = false
if (notification.appName && Data.Settings.ignoredApps && Data.Settings.ignoredApps.length > 0) {
for (var i = 0; i < Data.Settings.ignoredApps.length; i++) {
if (Data.Settings.ignoredApps[i].toLowerCase() === notification.appName.toLowerCase()) {
shouldIgnore = true
break
}
}
}
if (shouldIgnore) {
if (typeof notification.dismiss === 'function') {
notification.dismiss()
}
return
}
// Add to history and cleanup if needed
if (shell.notificationHistory) {
shell.addToNotificationHistory(notification, maxHistorySize)
if (shell.notificationHistory.count > cleanupThreshold) {
cleanupNotifications()
}
}
// Show notification window
if (shell.notificationWindow && shell.notificationWindow.screen === Quickshell.primaryScreen) {
shell.notificationWindow.visible = true
}
}
}
Component.onDestruction: {
if (shell.notificationHistory) {
shell.notificationHistory.clear()
}
}
}

View file

@ -0,0 +1,269 @@
import QtQuick
import "root:/Data" as Data
// Weather service using Open-Meteo API
Item {
id: service
property var shell
property string city: Data.Settings.weatherLocation
property bool isAmerican: Data.Settings.useFahrenheit
property int updateInterval: 3600 // 1 hour to reduce API calls
property string weatherDescription: ""
property var weather: null
property Timer retryTimer: Timer {
interval: 30000
repeat: false
running: false
onTriggered: getGeocoding()
}
Timer {
interval: service.updateInterval * 1000
running: true
repeat: true
triggeredOnStart: true
onTriggered: getGeocoding()
}
// Watch for settings changes and refresh weather data
Connections {
target: Data.Settings
function onWeatherLocationChanged() {
console.log("Weather location changed to:", Data.Settings.weatherLocation)
retryTimer.stop()
getGeocoding()
}
function onUseFahrenheitChanged() {
console.log("Temperature unit changed to:", Data.Settings.useFahrenheit ? "Fahrenheit" : "Celsius")
retryTimer.stop()
getGeocoding()
}
}
// WMO weather code descriptions (Open-Meteo standard)
property var weatherConsts: {
"omapiCodeDesc": {
0: "Clear sky",
1: "Mainly clear",
2: "Partly cloudy",
3: "Overcast",
45: "Fog",
48: "Depositing rime fog",
51: "Light drizzle",
53: "Moderate drizzle",
55: "Dense drizzle",
56: "Light freezing drizzle",
57: "Dense freezing drizzle",
61: "Slight rain",
63: "Moderate rain",
65: "Heavy rain",
66: "Light freezing rain",
67: "Heavy freezing rain",
71: "Slight snow fall",
73: "Moderate snow fall",
75: "Heavy snow fall",
77: "Snow grains",
80: "Slight rain showers",
81: "Moderate rain showers",
82: "Violent rain showers",
85: "Slight snow showers",
86: "Heavy snow showers",
95: "Thunderstorm",
96: "Thunderstorm with slight hail",
99: "Thunderstorm with heavy hail"
}
}
function getTemp(temp, tempUnit) {
return temp + tempUnit;
}
function updateWeather() {
if (!weather || !weather.current || !weather.current_units) {
console.warn("Weather data incomplete, skipping update");
return;
}
const weatherCode = weather.current.weather_code;
const temp = getTemp(Math.round(weather.current.temperature_2m || 0), weather.current_units.temperature_2m || "°C");
// Build 3-day forecast
const forecast = [];
const today = new Date();
if (weather.daily && weather.daily.time && weather.daily.weather_code && weather.daily.temperature_2m_min && weather.daily.temperature_2m_max) {
for (let i = 0; i < Math.min(3, weather.daily.time.length); i++) {
let dayName;
if (i === 0) {
dayName = "Today";
} else if (i === 1) {
dayName = "Tomorrow";
} else {
const futureDate = new Date(today);
futureDate.setDate(today.getDate() + i);
dayName = Qt.formatDate(futureDate, "ddd MMM d");
}
const dailyWeatherCode = weather.daily.weather_code[i];
const condition = weatherConsts.omapiCodeDesc[dailyWeatherCode] || "Unknown";
forecast.push({
dayName: dayName,
condition: condition,
minTemp: Math.round(weather.daily.temperature_2m_min[i]),
maxTemp: Math.round(weather.daily.temperature_2m_max[i])
});
}
}
// Update shell weather data in expected format
shell.weatherData = {
location: city,
currentTemp: temp,
currentCondition: weatherConsts.omapiCodeDesc[weatherCode] || "Unknown",
details: [
"Wind: " + Math.round(weather.current.wind_speed_10m || 0) + " km/h"
],
forecast: forecast
}
weatherDescription = weatherConsts.omapiCodeDesc[weatherCode] || "Unknown";
shell.weatherLoading = false;
}
// XHR pool to prevent memory leaks
property var activeXHRs: []
function cleanupXHR(xhr) {
if (xhr) {
xhr.abort();
xhr.onreadystatechange = null;
xhr.onerror = null;
const index = activeXHRs.indexOf(xhr);
if (index > -1) {
activeXHRs.splice(index, 1);
}
}
}
function getGeocoding() {
if (!city || city.trim() === "") {
console.warn("Weather location is empty, skipping weather request");
shell.weatherLoading = false;
return;
}
shell.weatherLoading = true;
const xhr = new XMLHttpRequest();
activeXHRs.push(xhr);
xhr.open("GET", `https://geocoding-api.open-meteo.com/v1/search?name=${city}&count=1&language=en&format=json`);
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
try {
const geocoding = JSON.parse(xhr.responseText);
if (geocoding.results && geocoding.results.length > 0) {
const lat = geocoding.results[0].latitude;
const lng = geocoding.results[0].longitude;
getWeather(lat, lng);
} else {
console.warn("No geocoding results found for location:", city);
retryTimer.running = true;
shell.weatherLoading = false;
}
} catch (e) {
console.error("Failed to parse geocoding response:", e);
retryTimer.running = true;
shell.weatherLoading = false;
}
} else if (xhr.status === 0) {
// Silent handling of network issues
if (!retryTimer.running) {
console.warn("Weather service: Network unavailable, will retry automatically");
}
retryTimer.running = true;
shell.weatherLoading = false;
} else {
console.error("Geocoding request failed with status:", xhr.status);
retryTimer.running = true;
shell.weatherLoading = false;
}
cleanupXHR(xhr);
}
};
xhr.onerror = function () {
console.error("Geocoding request failed with network error");
retryTimer.running = true;
shell.weatherLoading = false;
cleanupXHR(xhr);
};
xhr.send();
}
function getWeather(lat, lng) {
const xhr = new XMLHttpRequest();
activeXHRs.push(xhr);
xhr.open("GET", `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}&current=temperature_2m,is_day,weather_code,wind_speed_10m&daily=temperature_2m_max,temperature_2m_min,weather_code&forecast_days=3&temperature_unit=` + (isAmerican ? "fahrenheit" : "celsius"));
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
try {
weather = JSON.parse(xhr.responseText);
updateWeather();
} catch (e) {
console.error("Failed to parse weather response:", e);
retryTimer.running = true;
shell.weatherLoading = false;
}
} else if (xhr.status === 0) {
// Silent handling of network issues
if (!retryTimer.running) {
console.warn("Weather service: Network unavailable for weather data");
}
retryTimer.running = true;
shell.weatherLoading = false;
} else {
console.error("Weather request failed with status:", xhr.status);
retryTimer.running = true;
shell.weatherLoading = false;
}
cleanupXHR(xhr);
}
};
xhr.onerror = function () {
console.error("Weather request failed with network error");
retryTimer.running = true;
shell.weatherLoading = false;
cleanupXHR(xhr);
};
xhr.send();
}
function loadWeather() {
getGeocoding();
}
Component.onCompleted: getGeocoding()
Component.onDestruction: {
// Cleanup all active XHR requests
for (let i = 0; i < activeXHRs.length; i++) {
if (activeXHRs[i]) {
activeXHRs[i].abort();
activeXHRs[i].onreadystatechange = null;
activeXHRs[i].onerror = null;
}
}
activeXHRs = [];
weather = null;
shell.weatherData = null;
weatherDescription = "";
}
}

View file

@ -0,0 +1,125 @@
// Calendar.qml
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import "root:/Data" as Data
// Calendar widget with navigation
Rectangle {
id: calendarRoot
property var shell
radius: 20
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
readonly property date currentDate: new Date()
property int month: currentDate.getMonth()
property int year: currentDate.getFullYear()
readonly property int currentDay: currentDate.getDate()
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 12
// Month/Year header
RowLayout {
Layout.fillWidth: true
spacing: 8
// Reusable navigation button
component NavButton: AbstractButton {
property alias buttonText: buttonLabel.text
implicitWidth: 30
implicitHeight: 30
background: Rectangle {
radius: 15
color: parent.down ? Qt.darker(Data.ThemeManager.accentColor, 1.2) :
parent.hovered ? Qt.lighter(Data.ThemeManager.highlightBg, 1.1) : Data.ThemeManager.highlightBg
}
Text {
id: buttonLabel
anchors.centerIn: parent
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
}
}
// Current month and year display
Text {
text: Qt.locale("en_US").monthName(calendarRoot.month) + " " + calendarRoot.year
color: Data.ThemeManager.accentColor
font.bold: true
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: 18
}
}
// Weekday headers (Monday-Sunday)
Grid {
columns: 7
rowSpacing: 4
columnSpacing: 0
Layout.leftMargin: 2
Layout.fillWidth: true
Repeater {
model: ["M", "T", "W", "T", "F", "S", "S"]
delegate: Text {
text: modelData
color: Data.ThemeManager.fgColor
font.bold: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width / 7
font.pixelSize: 14
}
}
}
// Calendar grid
MonthGrid {
id: monthGrid
month: calendarRoot.month
year: calendarRoot.year
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 4
leftPadding: 0
rightPadding: 0
locale: Qt.locale("en_US")
implicitHeight: 400
delegate: Rectangle {
width: 30
height: 30
radius: 15
readonly property bool isCurrentMonth: model.month === calendarRoot.month
readonly property bool isToday: model.day === calendarRoot.currentDay &&
model.month === calendarRoot.currentDate.getMonth() &&
calendarRoot.year === calendarRoot.currentDate.getFullYear() &&
isCurrentMonth
// Dynamic styling: today = accent color, current month = normal, other months = dimmed
color: isToday ? Data.ThemeManager.accentColor :
isCurrentMonth ? Data.ThemeManager.bgColor : Qt.darker(Data.ThemeManager.bgColor, 1.4)
Text {
text: model.day
anchors.centerIn: parent
color: isToday ? Data.ThemeManager.bgColor :
isCurrentMonth ? Data.ThemeManager.fgColor : Qt.darker(Data.ThemeManager.fgColor, 1.5)
font.bold: isToday
font.pixelSize: 14
font.family: "Roboto"
}
}
}
}
}

View file

@ -0,0 +1,121 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// Calendar popup with animations
Popup {
id: calendarPopup
property bool hovered: false
property bool clickMode: false // Persistent mode - stays open until clicked again
property var shell
property int targetX: 0
readonly property int targetY: Screen.height - height
width: 280
height: 280
modal: false
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
padding: 15
// Animation state properties
property bool _visible: false
property real animX: targetX - 20
property real animOpacity: 0
x: animX
y: targetY
opacity: animOpacity
visible: _visible
// Smooth slide-in animation
Behavior on animX {
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
}
Behavior on animOpacity {
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
}
// Hover mode: show/hide based on mouse state
onHoveredChanged: {
if (!clickMode) {
if (hovered) {
_visible = true
animX = targetX
animOpacity = 1
} else {
animX = targetX - 20
animOpacity = 0
}
}
}
// Click mode: persistent visibility toggle
onClickModeChanged: {
if (clickMode) {
_visible = true
animX = targetX
animOpacity = 1
} else {
animX = targetX - 20
animOpacity = 0
}
}
// Hide when animation completes
onAnimOpacityChanged: {
if (animOpacity === 0 && !hovered && !clickMode) {
_visible = false
}
}
function setHovered(state) {
hovered = state
}
function setClickMode(state) {
clickMode = state
}
// Hover detection
MouseArea {
id: hoverArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
anchors.margins: 10 // Larger area to reduce flicker
onEntered: {
if (!clickMode) {
setHovered(true)
}
}
onExited: {
if (!clickMode) {
// Delayed exit check to prevent hover flicker
Qt.callLater(() => {
if (!hoverArea.containsMouse) {
setHovered(false)
}
})
}
}
}
// Lazy-loaded calendar content
Loader {
anchors.fill: parent
active: calendarPopup._visible
source: active ? "Calendar.qml" : ""
onLoaded: {
if (item) {
item.shell = calendarPopup.shell
}
}
}
background: Rectangle {
color: Data.ThemeManager.bgColor
topRightRadius: 20
}
}

View file

@ -0,0 +1,64 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import "root:/Data" as Data
import "root:/Core" as Core
// Clock with border integration
Item {
id: clockRoot
width: clockBackground.width
height: clockBackground.height
Rectangle {
id: clockBackground
width: clockText.implicitWidth + 24
height: 32
color: Data.ThemeManager.bgColor
// Rounded corner for border integration
topRightRadius: height / 2
Text {
id: clockText
anchors.centerIn: parent
font.family: "Roboto"
font.pixelSize: 14
font.bold: true
color: Data.ThemeManager.accentColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: Qt.formatTime(new Date(), "HH:mm")
}
}
// Update every minute
Timer {
interval: 60000
running: true
repeat: true
onTriggered: clockText.text = Qt.formatTime(new Date(), "HH:mm")
}
// Border integration corner pieces
Core.Corners {
id: topLeftCorner
position: "topleft"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: -39
offsetY: -26
z: 0 // Same z-level as clock background
}
Core.Corners {
id: topLeftCorner2
position: "topleft"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: 20
offsetY: 6
z: 0 // Same z-level as clock background
}
}

View file

@ -0,0 +1,136 @@
import QtQuick
import "root:/Data" as Data
import "root:/Core" as Core
// Main control panel coordinator - handles recording and system actions
Item {
id: controlPanelContainer
required property var shell
property bool isRecording: false
property int currentTab: 0 // 0=main, 1=calendar, 2=clipboard, 3=notifications, 4=wallpapers, 5=music, 6=settings
property var tabIcons: ["widgets", "calendar_month", "content_paste", "notifications", "wallpaper", "music_note", "settings"]
property bool isShown: false
property var recordingProcess: null
signal recordingRequested()
signal stopRecordingRequested()
signal systemActionRequested(string action)
signal performanceActionRequested(string action)
// Screen recording
onRecordingRequested: {
var currentDate = new Date()
var hours = String(currentDate.getHours()).padStart(2, '0')
var minutes = String(currentDate.getMinutes()).padStart(2, '0')
var day = String(currentDate.getDate()).padStart(2, '0')
var month = String(currentDate.getMonth() + 1).padStart(2, '0')
var year = currentDate.getFullYear()
var filename = hours + "-" + minutes + "-" + day + "-" + month + "-" + year + ".mp4"
var outputPath = Data.Settings.videoPath + filename
var command = "gpu-screen-recorder -w portal -f 60 -a default_output -o " + outputPath
var qmlString = 'import Quickshell.Io; Process { command: ["sh", "-c", "' + command + '"]; running: true }'
try {
recordingProcess = Qt.createQmlObject(qmlString, controlPanelContainer)
isRecording = true
} catch (e) {
console.error("Failed to start recording:", e)
}
}
// Stop recording with cleanup
onStopRecordingRequested: {
if (recordingProcess && isRecording) {
var stopQmlString = 'import Quickshell.Io; Process { command: ["sh", "-c", "pkill -SIGINT -f \'gpu-screen-recorder.*portal\'"]; running: true; onExited: function() { destroy() } }'
try {
var stopProcess = Qt.createQmlObject(stopQmlString, controlPanelContainer)
var cleanupTimer = Qt.createQmlObject('import QtQuick; Timer { interval: 3000; running: true; repeat: false }', controlPanelContainer)
cleanupTimer.triggered.connect(function() {
if (recordingProcess) {
recordingProcess.running = false
recordingProcess.destroy()
recordingProcess = null
}
var forceKillQml = 'import Quickshell.Io; Process { command: ["sh", "-c", "pkill -9 -f \'gpu-screen-recorder.*portal\' 2>/dev/null || true"]; running: true; onExited: function() { destroy() } }'
var forceKillProcess = Qt.createQmlObject(forceKillQml, controlPanelContainer)
cleanupTimer.destroy()
})
} catch (e) {
console.error("Failed to stop recording:", e)
}
}
isRecording = false
}
// System action routing
onSystemActionRequested: function(action) {
switch(action) {
case "lock":
Core.ProcessManager.lock()
break
case "reboot":
Core.ProcessManager.reboot()
break
case "shutdown":
Core.ProcessManager.shutdown()
break
}
}
onPerformanceActionRequested: function(action) {
console.log("Performance action requested:", action)
}
// Control panel window component
ControlPanelWindow {
id: controlPanelWindow
// Pass through properties
shell: controlPanelContainer.shell
isRecording: controlPanelContainer.isRecording
currentTab: controlPanelContainer.currentTab
tabIcons: controlPanelContainer.tabIcons
isShown: controlPanelContainer.isShown
// Bind state changes back to parent
onCurrentTabChanged: controlPanelContainer.currentTab = currentTab
onIsShownChanged: controlPanelContainer.isShown = isShown
// Forward signals
onRecordingRequested: controlPanelContainer.recordingRequested()
onStopRecordingRequested: controlPanelContainer.stopRecordingRequested()
onSystemActionRequested: function(action) { controlPanelContainer.systemActionRequested(action) }
onPerformanceActionRequested: function(action) { controlPanelContainer.performanceActionRequested(action) }
}
// Clean up processes on destruction
Component.onDestruction: {
if (recordingProcess) {
try {
if (recordingProcess.running) {
recordingProcess.terminate()
}
recordingProcess.destroy()
} catch (e) {
console.warn("Error cleaning up recording process:", e)
}
recordingProcess = null
}
// Force kill any remaining gpu-screen-recorder processes
var forceCleanupCmd = 'import Quickshell.Io; Process { command: ["sh", "-c", "pkill -9 -f gpu-screen-recorder 2>/dev/null || true"]; running: true; onExited: function() { destroy() } }'
try {
Qt.createQmlObject(forceCleanupCmd, controlPanelContainer)
} catch (e) {
console.warn("Error in force cleanup:", e)
}
}
}

View file

@ -0,0 +1,106 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import "root:/Data" as Data
import "./components/navigation" as Navigation
// Panel content with tab layout - now clean and organized!
Item {
id: contentRoot
// Properties passed from parent
required property var shell
required property bool isRecording
property int currentTab: 0
property var tabIcons: []
required property var triggerMouseArea
// Signals to forward to parent
signal recordingRequested()
signal stopRecordingRequested()
signal systemActionRequested(string action)
signal performanceActionRequested(string action)
// Hover detection for auto-hide
property bool isHovered: {
const mouseStates = {
triggerHovered: triggerMouseArea.containsMouse,
backgroundHovered: backgroundMouseArea.containsMouse,
tabSidebarHovered: tabNavigation.containsMouse,
tabContainerHovered: tabContainer.isHovered,
tabContentActive: currentTab !== 0, // Non-main tabs stay open
tabNavigationActive: tabNavigation.containsMouse
}
return Object.values(mouseStates).some(state => state)
}
// Expose text input focus state for keyboard management
property bool textInputFocused: tabContainer.textInputFocused
// Panel background with bottom-only rounded corners
Rectangle {
anchors.fill: parent
color: Data.ThemeManager.bgColor
topLeftRadius: 0
topRightRadius: 0
bottomLeftRadius: 20
bottomRightRadius: 20
z: -10 // Far behind everything to avoid layering conflicts
}
// Main content container with tab layout
Rectangle {
id: mainContainer
anchors.fill: parent
anchors.margins: 9
color: "transparent"
radius: 12
MouseArea {
id: backgroundMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
property alias containsMouse: backgroundMouseArea.containsMouse
}
// Left sidebar with tab navigation
Navigation.TabNavigation {
id: tabNavigation
width: 40
height: parent.height
anchors.left: parent.left
anchors.leftMargin: 9
anchors.top: parent.top
anchors.topMargin: 18
currentTab: contentRoot.currentTab
tabIcons: contentRoot.tabIcons
onCurrentTabChanged: contentRoot.currentTab = currentTab
}
// Main tab content area with sliding animation
Navigation.TabContainer {
id: tabContainer
width: parent.width - tabNavigation.width - 45
height: parent.height - 36
anchors.left: tabNavigation.right
anchors.leftMargin: 9
anchors.top: parent.top
anchors.topMargin: 18
shell: contentRoot.shell
isRecording: contentRoot.isRecording
triggerMouseArea: contentRoot.triggerMouseArea
currentTab: contentRoot.currentTab
onRecordingRequested: contentRoot.recordingRequested()
onStopRecordingRequested: contentRoot.stopRecordingRequested()
onSystemActionRequested: function(action) { contentRoot.systemActionRequested(action) }
onPerformanceActionRequested: function(action) { contentRoot.performanceActionRequested(action) }
}
}
}

View file

@ -0,0 +1,171 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Wayland
import "root:/Data" as Data
import "root:/Core" as Core
// Control panel window and trigger
PanelWindow {
id: controlPanelWindow
// Properties passed from parent ControlPanel
required property var shell
required property bool isRecording
property int currentTab: 0
property var tabIcons: []
property bool isShown: false
// Signals to forward to parent
signal recordingRequested()
signal stopRecordingRequested()
signal systemActionRequested(string action)
signal performanceActionRequested(string action)
screen: Quickshell.primaryScreen || Quickshell.screens[0]
anchors.top: true
anchors.left: true
anchors.right: true
margins.bottom: 0
margins.left: (screen ? screen.width / 2 - 400 : 0) // Centered
margins.right: (screen ? screen.width / 2 - 400 : 0)
implicitWidth: 640
implicitHeight: isShown ? 400 : 8 // Expand/collapse animation
Behavior on implicitHeight {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
exclusiveZone: (panelContent && panelContent.textInputFocused) ? -1 : 0
color: "transparent"
visible: true
WlrLayershell.namespace: "quickshell-controlpanel"
WlrLayershell.keyboardFocus: (panelContent && panelContent.textInputFocused) ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.OnDemand
// Hover trigger area at screen top
MouseArea {
id: triggerMouseArea
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
width: 600
height: 8
hoverEnabled: true
onContainsMouseChanged: {
if (containsMouse) {
show()
}
}
}
// Main panel content
ControlPanelContent {
id: panelContent
width: 600
height: 380
anchors.top: parent.top
anchors.topMargin: 8 // Trigger area space
anchors.horizontalCenter: parent.horizontalCenter
visible: isShown
opacity: isShown ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
// Pass through properties
shell: controlPanelWindow.shell
isRecording: controlPanelWindow.isRecording
currentTab: controlPanelWindow.currentTab
tabIcons: controlPanelWindow.tabIcons
triggerMouseArea: triggerMouseArea
// Bind state changes
onCurrentTabChanged: controlPanelWindow.currentTab = currentTab
// Forward signals
onRecordingRequested: controlPanelWindow.recordingRequested()
onStopRecordingRequested: controlPanelWindow.stopRecordingRequested()
onSystemActionRequested: function(action) { controlPanelWindow.systemActionRequested(action) }
onPerformanceActionRequested: function(action) { controlPanelWindow.performanceActionRequested(action) }
// Hover state management
onIsHoveredChanged: {
if (isHovered) {
hideTimer.stop()
} else {
hideTimer.restart()
}
}
}
// Border integration corners (positioned to match panel edges)
Core.Corners {
id: controlPanelLeftCorner
position: "bottomright"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: -661
offsetY: -313
visible: isShown
z: 1 // Higher z-index to render above shadow effects
// Disable implicit animations to prevent corner sliding
Behavior on x { enabled: false }
Behavior on y { enabled: false }
}
Core.Corners {
id: controlPanelRightCorner
position: "bottomleft"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: 661
offsetY: -313
visible: isShown
z: 1 // Higher z-index to render above shadow effects
Behavior on x { enabled: false }
Behavior on y { enabled: false }
}
// Auto-hide timer
Timer {
id: hideTimer
interval: 400
repeat: false
onTriggered: hide()
}
function show() {
if (isShown) return
isShown = true
hideTimer.stop()
}
function hide() {
if (!isShown) return
// Only hide if on main tab and nothing is being hovered
if (currentTab === 0 && !panelContent.isHovered && !triggerMouseArea.containsMouse) {
isShown = false
}
// For non-main tabs, only hide if explicitly not hovered and no trigger hover
else if (currentTab !== 0 && !panelContent.isHovered && !triggerMouseArea.containsMouse) {
// Add delay for non-main tabs to prevent accidental hiding
Qt.callLater(function() {
if (!panelContent.isHovered && !triggerMouseArea.containsMouse) {
isShown = false
}
})
}
}
}

View file

@ -0,0 +1,107 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
import "." as Controls
// Dual-section control panel
Row {
id: root
spacing: 16
visible: true
height: 80
required property bool isRecording
required property var shell
signal performanceActionRequested(string action)
signal systemActionRequested(string action)
signal mouseChanged(bool containsMouse)
// Combined hover state from both sections
property bool containsMouse: performanceSection.containsMouse || systemSection.containsMouse
onContainsMouseChanged: mouseChanged(containsMouse)
// Performance controls section (left half)
Rectangle {
id: performanceSection
width: (parent.width - parent.spacing) / 2
height: parent.height
radius: 20
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
visible: true
// Hover tracking with coordination between background and content
property bool containsMouse: performanceMouseArea.containsMouse || performanceControls.containsMouse
MouseArea {
id: performanceMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onContainsMouseChanged: {
if (containsMouse) {
performanceSection.containsMouse = true
} else if (!performanceControls.containsMouse) {
performanceSection.containsMouse = false
}
}
}
Controls.PerformanceControls {
id: performanceControls
anchors.fill: parent
anchors.margins: 12
shell: root.shell
onPerformanceActionRequested: function(action) { root.performanceActionRequested(action) }
onMouseChanged: function(containsMouse) {
if (containsMouse) {
performanceSection.containsMouse = true
} else if (!performanceMouseArea.containsMouse) {
performanceSection.containsMouse = false
}
}
}
}
// System controls section (right half)
Rectangle {
id: systemSection
width: (parent.width - parent.spacing) / 2
height: parent.height
radius: 20
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
visible: true
// Hover tracking with coordination between background and content
property bool containsMouse: systemMouseArea.containsMouse || systemControls.containsMouse
MouseArea {
id: systemMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onContainsMouseChanged: {
if (containsMouse) {
systemSection.containsMouse = true
} else if (!systemControls.containsMouse) {
systemSection.containsMouse = false
}
}
}
Controls.SystemControls {
id: systemControls
anchors.fill: parent
anchors.margins: 12
shell: root.shell
onSystemActionRequested: function(action) { root.systemActionRequested(action) }
onMouseChanged: function(containsMouse) {
if (containsMouse) {
systemSection.containsMouse = true
} else if (!systemMouseArea.containsMouse) {
systemSection.containsMouse = false
}
}
}
}
}

View file

@ -0,0 +1,127 @@
import QtQuick
import QtQuick.Controls
import Quickshell.Services.UPower
// Power profile controls
Column {
id: root
required property var shell
spacing: 8
signal performanceActionRequested(string action)
signal mouseChanged(bool containsMouse)
readonly property bool containsMouse: performanceButton.containsMouse ||
balancedButton.containsMouse ||
powerSaverButton.containsMouse
// Safe UPower service access with fallback checks
readonly property bool upowerReady: typeof PowerProfiles !== 'undefined' && PowerProfiles
readonly property int currentProfile: upowerReady ? PowerProfiles.profile : 0
onContainsMouseChanged: root.mouseChanged(containsMouse)
opacity: visible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
Row {
spacing: 8
width: parent.width
// Performance mode button
SystemButton {
id: performanceButton
width: (parent.width - parent.spacing * 2) / 3
height: 52
shell: root.shell
iconText: "speed"
isActive: root.upowerReady && (typeof PowerProfile !== 'undefined') ?
root.currentProfile === PowerProfile.Performance : false
onClicked: {
if (root.upowerReady && typeof PowerProfile !== 'undefined') {
PowerProfiles.profile = PowerProfile.Performance
root.performanceActionRequested("performance")
} else {
console.warn("PowerProfiles not available")
}
}
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
// Balanced mode button
SystemButton {
id: balancedButton
width: (parent.width - parent.spacing * 2) / 3
height: 52
shell: root.shell
iconText: "balance"
isActive: root.upowerReady && (typeof PowerProfile !== 'undefined') ?
root.currentProfile === PowerProfile.Balanced : false
onClicked: {
if (root.upowerReady && typeof PowerProfile !== 'undefined') {
PowerProfiles.profile = PowerProfile.Balanced
root.performanceActionRequested("balanced")
} else {
console.warn("PowerProfiles not available")
}
}
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
// Power saver mode button
SystemButton {
id: powerSaverButton
width: (parent.width - parent.spacing * 2) / 3
height: 52
shell: root.shell
iconText: "battery_saver"
isActive: root.upowerReady && (typeof PowerProfile !== 'undefined') ?
root.currentProfile === PowerProfile.PowerSaver : false
onClicked: {
if (root.upowerReady && typeof PowerProfile !== 'undefined') {
PowerProfiles.profile = PowerProfile.PowerSaver
root.performanceActionRequested("powersaver")
} else {
console.warn("PowerProfiles not available")
}
}
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
}
// Ensure UPower service initialization
Component.onCompleted: {
Qt.callLater(function() {
if (!root.upowerReady) {
console.warn("UPower service not ready - performance controls may not work correctly")
}
})
}
}

View file

@ -0,0 +1,122 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// System button
Rectangle {
id: root
required property var shell
required property string iconText
property string labelText: ""
property bool isActive: false
radius: 20
// Dynamic color based on active and hover states
color: {
if (isActive) {
return mouseArea.containsMouse ?
Qt.lighter(Data.ThemeManager.accentColor, 1.1) :
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
} else {
return mouseArea.containsMouse ?
Qt.lighter(Data.ThemeManager.accentColor, 1.2) :
Qt.lighter(Data.ThemeManager.bgColor, 1.15)
}
}
border.width: isActive ? 2 : 1
border.color: isActive ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.3)
signal clicked()
signal mouseChanged(bool containsMouse)
property bool isHovered: mouseArea.containsMouse
readonly property alias containsMouse: mouseArea.containsMouse
// Smooth color transitions
Behavior on color {
ColorAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
Behavior on border.color {
ColorAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
// Hover scale animation
scale: isHovered ? 1.05 : 1.0
Behavior on scale {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
// Button content with icon and optional label
Column {
anchors.centerIn: parent
spacing: 2
// System action icon
Text {
text: root.iconText
font.family: "Material Symbols Outlined"
font.pixelSize: 16
anchors.horizontalCenter: parent.horizontalCenter
color: {
if (root.isActive) {
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
} else {
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
}
}
Behavior on color {
ColorAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
// Optional text label
Label {
text: root.labelText
font.family: "Roboto"
font.pixelSize: 8
color: {
if (root.isActive) {
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
} else {
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
}
}
anchors.horizontalCenter: parent.horizontalCenter
font.weight: root.isActive ? Font.Bold : Font.Medium
visible: root.labelText !== ""
Behavior on color {
ColorAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
}
// Click and hover handling
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onContainsMouseChanged: root.mouseChanged(containsMouse)
onClicked: root.clicked()
}
}

View file

@ -0,0 +1,93 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
// System action buttons
RowLayout {
id: root
required property var shell
spacing: 8
signal systemActionRequested(string action)
signal mouseChanged(bool containsMouse)
readonly property bool containsMouse: lockButton.containsMouse ||
rebootButton.containsMouse ||
shutdownButton.containsMouse
onContainsMouseChanged: root.mouseChanged(containsMouse)
opacity: visible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
// Lock Button
SystemButton {
id: lockButton
Layout.fillHeight: true
Layout.fillWidth: true
shell: root.shell
iconText: "lock"
onClicked: {
console.log("Lock button clicked")
console.log("root.shell:", root.shell)
console.log("root.shell.lockscreen:", root.shell ? root.shell.lockscreen : "shell is null")
// Directly trigger custom lockscreen
if (root.shell && root.shell.lockscreen) {
console.log("Calling root.shell.lockscreen.lock()")
root.shell.lockscreen.lock()
} else {
console.log("Fallback to systemActionRequested")
// Fallback to system action for compatibility
root.systemActionRequested("lock")
}
}
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
// Reboot Button
SystemButton {
id: rebootButton
Layout.fillHeight: true
Layout.fillWidth: true
shell: root.shell
iconText: "restart_alt"
onClicked: root.systemActionRequested("reboot")
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
// Shutdown Button
SystemButton {
id: shutdownButton
Layout.fillHeight: true
Layout.fillWidth: true
shell: root.shell
iconText: "power_settings_new"
onClicked: root.systemActionRequested("shutdown")
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
}

View file

@ -0,0 +1,666 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Services.Mpris
import "root:/Data" as Data
// Music player with MPRIS integration
Rectangle {
id: musicPlayer
property var shell
property var currentPlayer: null
property real currentPosition: 0
property int selectedPlayerIndex: 0
color: "transparent"
// Get all available players
function getAvailablePlayers() {
if (!Mpris.players || !Mpris.players.values) {
return []
}
let allPlayers = Mpris.players.values
let controllablePlayers = []
for (let i = 0; i < allPlayers.length; i++) {
let player = allPlayers[i]
if (player && player.canControl) {
controllablePlayers.push(player)
}
}
return controllablePlayers
}
// Find the active player (either selected or first available)
function findActivePlayer() {
let availablePlayers = getAvailablePlayers()
if (availablePlayers.length === 0) {
return null
}
// Auto-switch to playing player if enabled
if (Data.Settings.autoSwitchPlayer) {
for (let i = 0; i < availablePlayers.length; i++) {
if (availablePlayers[i].isPlaying) {
selectedPlayerIndex = i
return availablePlayers[i]
}
}
}
// Use selected player if valid, otherwise use first available
if (selectedPlayerIndex < availablePlayers.length) {
return availablePlayers[selectedPlayerIndex]
} else {
selectedPlayerIndex = 0
return availablePlayers[0]
}
}
// Update current player
function updateCurrentPlayer() {
let newPlayer = findActivePlayer()
if (newPlayer !== currentPlayer) {
currentPlayer = newPlayer
currentPosition = currentPlayer ? currentPlayer.position : 0
}
}
// Timer to update progress bar position
Timer {
id: positionTimer
interval: 1000
running: currentPlayer && currentPlayer.isPlaying
repeat: true
onTriggered: {
if (currentPlayer) {
currentPosition = currentPlayer.position
}
}
}
// Timer to check for auto-switching to playing players
Timer {
id: autoSwitchTimer
interval: 2000 // Check every 2 seconds
running: Data.Settings.autoSwitchPlayer
repeat: true
onTriggered: {
if (Data.Settings.autoSwitchPlayer) {
let availablePlayers = getAvailablePlayers()
for (let i = 0; i < availablePlayers.length; i++) {
if (availablePlayers[i].isPlaying && selectedPlayerIndex !== i) {
selectedPlayerIndex = i
updateCurrentPlayer()
updatePlayerList()
break
}
}
}
}
}
// Update player list for dropdown
function updatePlayerList() {
if (!playerComboBox) return
let availablePlayers = getAvailablePlayers()
let playerNames = availablePlayers.map(player => player.identity || "Unknown Player")
playerComboBox.model = playerNames
if (selectedPlayerIndex >= playerNames.length) {
selectedPlayerIndex = 0
}
playerComboBox.currentIndex = selectedPlayerIndex
}
// Monitor for player changes
Connections {
target: Mpris.players
function onValuesChanged() {
updatePlayerList()
updateCurrentPlayer()
}
function onRowsInserted() {
updatePlayerList()
updateCurrentPlayer()
}
function onRowsRemoved() {
updatePlayerList()
updateCurrentPlayer()
}
function onObjectInsertedPost() {
updatePlayerList()
updateCurrentPlayer()
}
function onObjectRemovedPost() {
updatePlayerList()
updateCurrentPlayer()
}
}
// Monitor for settings changes
Connections {
target: Data.Settings
function onAutoSwitchPlayerChanged() {
console.log("Auto-switch player setting changed to:", Data.Settings.autoSwitchPlayer)
updateCurrentPlayer()
}
function onAlwaysShowPlayerDropdownChanged() {
console.log("Always show dropdown setting changed to:", Data.Settings.alwaysShowPlayerDropdown)
// Dropdown visibility is automatically handled by the binding
}
}
Component.onCompleted: {
updatePlayerList()
updateCurrentPlayer()
}
Column {
anchors.fill: parent
spacing: 10
// No music player available state
Item {
width: parent.width
height: parent.height
visible: !currentPlayer
Column {
anchors.centerIn: parent
spacing: 16
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "music_note"
font.family: "Material Symbols Outlined"
font.pixelSize: 48
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: getAvailablePlayers().length > 0 ? "No controllable player selected" : "No music player detected"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
font.family: "Roboto"
font.pixelSize: 14
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: getAvailablePlayers().length > 0 ? "Select a player from the dropdown above" : "Start a music player to see controls"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.4)
font.family: "Roboto"
font.pixelSize: 12
}
}
}
// Music player controls
Column {
width: parent.width
spacing: 12
visible: currentPlayer
// Player info and artwork
Rectangle {
width: parent.width
height: 130
radius: 20
color: Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2)
border.width: 1
Row {
anchors.fill: parent
anchors.margins: 16
spacing: 16
// Album artwork
Rectangle {
id: albumArtwork
width: 90
height: 90
radius: 20
color: Qt.darker(Data.ThemeManager.bgColor, 1.3)
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
border.width: 1
Image {
id: albumArt
anchors.fill: parent
anchors.margins: 2
fillMode: Image.PreserveAspectCrop
smooth: true
source: currentPlayer ? (currentPlayer.trackArtUrl || "") : ""
visible: source.toString() !== ""
// Rounded corners using layer
layer.enabled: true
layer.effect: OpacityMask {
cached: true // Cache to reduce ShaderEffect issues
maskSource: Rectangle {
width: albumArt.width
height: albumArt.height
radius: 20
visible: false
}
}
}
// Fallback music icon
Text {
anchors.centerIn: parent
text: "album"
font.family: "Material Symbols Outlined"
font.pixelSize: 32
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.4)
visible: !albumArt.visible
}
}
// Track info
Column {
width: parent.width - albumArtwork.width - parent.spacing
height: parent.height
spacing: 4
Text {
width: parent.width
text: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : ""
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 18
font.bold: true
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
}
Text {
width: parent.width
text: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : ""
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.8)
font.family: "Roboto"
font.pixelSize: 18
elide: Text.ElideRight
}
Text {
width: parent.width
text: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : ""
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
font.family: "Roboto"
font.pixelSize: 15
elide: Text.ElideRight
}
}
}
}
// Interactive progress bar with seek functionality
Rectangle {
id: progressBarBackground
width: parent.width
height: 8
radius: 20
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.15)
property real progressRatio: currentPlayer && currentPlayer.length > 0 ?
(currentPosition / currentPlayer.length) : 0
Rectangle {
id: progressFill
width: progressBarBackground.progressRatio * parent.width
height: parent.height
radius: parent.radius
color: Data.ThemeManager.accentColor
Behavior on width {
NumberAnimation { duration: 200 }
}
}
// Interactive progress handle (circle)
Rectangle {
id: progressHandle
width: 16
height: 16
radius: 8
color: Data.ThemeManager.accentColor
border.color: Qt.lighter(Data.ThemeManager.accentColor, 1.3)
border.width: 1
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width/2))
anchors.verticalCenter: parent.verticalCenter
visible: currentPlayer && currentPlayer.length > 0
scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0
Behavior on scale {
NumberAnimation { duration: 150 }
}
}
// Mouse area for seeking
MouseArea {
id: progressMouseArea
anchors.fill: parent
hoverEnabled: true
enabled: currentPlayer && currentPlayer.length > 0 && currentPlayer.canSeek
onClicked: function(mouse) {
if (currentPlayer && currentPlayer.length > 0) {
let ratio = mouse.x / width
let seekPosition = ratio * currentPlayer.length
currentPlayer.position = seekPosition
currentPosition = seekPosition
}
}
onPositionChanged: function(mouse) {
if (pressed && currentPlayer && currentPlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / width))
let seekPosition = ratio * currentPlayer.length
currentPlayer.position = seekPosition
currentPosition = seekPosition
}
}
}
}
// Player selection dropdown (conditional visibility)
Rectangle {
width: parent.width
height: 38
radius: 20
color: Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2)
border.width: 1
visible: {
let playerCount = getAvailablePlayers().length
let alwaysShow = Data.Settings.alwaysShowPlayerDropdown
let shouldShow = alwaysShow || playerCount > 1
return shouldShow
}
Row {
anchors.fill: parent
anchors.margins: 6
anchors.leftMargin: 12
spacing: 8
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Player:"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.family: "Roboto"
font.pixelSize: 12
font.bold: true
}
ComboBox {
id: playerComboBox
anchors.verticalCenter: parent.verticalCenter
width: parent.width - parent.children[0].width - parent.spacing
height: 26
model: []
onActivated: function(index) {
selectedPlayerIndex = index
updateCurrentPlayer()
}
background: Rectangle {
color: Qt.darker(Data.ThemeManager.bgColor, 1.3)
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2)
border.width: 1
radius: 20
}
contentItem: Text {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.right: parent.right
anchors.rightMargin: 22
anchors.verticalCenter: parent.verticalCenter
text: playerComboBox.currentText || "No players"
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
font.bold: true
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
indicator: Text {
anchors.right: parent.right
anchors.rightMargin: 4
anchors.verticalCenter: parent.verticalCenter
text: "expand_more"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
}
popup: Popup {
y: playerComboBox.height + 2
width: playerComboBox.width
implicitHeight: contentItem.implicitHeight + 4
background: Rectangle {
color: Qt.darker(Data.ThemeManager.bgColor, 1.2)
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
border.width: 1
radius: 20
}
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: playerComboBox.popup.visible ? playerComboBox.delegateModel : null
currentIndex: playerComboBox.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator { }
}
}
delegate: ItemDelegate {
width: playerComboBox.width
height: 28
background: Rectangle {
color: parent.hovered ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.15) : "transparent"
radius: 20
}
contentItem: Text {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
text: modelData || ""
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
font.bold: true
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
}
}
}
}
// Media controls
Row {
width: parent.width
height: 35
spacing: 6
// Previous button
Rectangle {
width: (parent.width - parent.spacing * 4) * 0.2
height: parent.height
radius: height / 2
color: previousButton.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
border.width: 1
MouseArea {
id: previousButton
anchors.fill: parent
hoverEnabled: true
enabled: currentPlayer && currentPlayer.canGoPrevious
onClicked: if (currentPlayer) currentPlayer.previous()
}
Text {
anchors.centerIn: parent
text: "skip_previous"
font.family: "Material Symbols Outlined"
font.pixelSize: 18
color: previousButton.enabled ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
}
}
// Play/Pause button
Rectangle {
width: (parent.width - parent.spacing * 4) * 0.3
height: parent.height
radius: height / 2
color: playButton.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: Data.ThemeManager.accentColor
border.width: 2
MouseArea {
id: playButton
anchors.fill: parent
hoverEnabled: true
enabled: currentPlayer && (currentPlayer.canPlay || currentPlayer.canPause)
onClicked: {
if (currentPlayer) {
if (currentPlayer.isPlaying) {
currentPlayer.pause()
} else {
currentPlayer.play()
}
}
}
}
Text {
anchors.centerIn: parent
text: currentPlayer && currentPlayer.isPlaying ? "pause" : "play_arrow"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: playButton.enabled ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
}
}
// Next button
Rectangle {
width: (parent.width - parent.spacing * 4) * 0.2
height: parent.height
radius: height / 2
color: nextButton.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
border.width: 1
MouseArea {
id: nextButton
anchors.fill: parent
hoverEnabled: true
enabled: currentPlayer && currentPlayer.canGoNext
onClicked: if (currentPlayer) currentPlayer.next()
}
Text {
anchors.centerIn: parent
text: "skip_next"
font.family: "Material Symbols Outlined"
font.pixelSize: 18
color: nextButton.enabled ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
}
}
// Shuffle button
Rectangle {
width: (parent.width - parent.spacing * 4) * 0.15
height: parent.height
radius: height / 2
color: shuffleButton.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: currentPlayer && currentPlayer.shuffle ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
border.width: 1
MouseArea {
id: shuffleButton
anchors.fill: parent
hoverEnabled: true
enabled: currentPlayer && currentPlayer.canControl && currentPlayer.shuffleSupported
onClicked: {
if (currentPlayer && currentPlayer.shuffleSupported) {
currentPlayer.shuffle = !currentPlayer.shuffle
}
}
}
Text {
anchors.centerIn: parent
text: "shuffle"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: shuffleButton.enabled ?
(currentPlayer && currentPlayer.shuffle ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
}
}
// Repeat button
Rectangle {
width: (parent.width - parent.spacing * 4) * 0.15
height: parent.height
radius: height / 2
color: repeatButton.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: currentPlayer && currentPlayer.loopState !== MprisLoopState.None ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
border.width: 1
MouseArea {
id: repeatButton
anchors.fill: parent
hoverEnabled: true
enabled: currentPlayer && currentPlayer.canControl && currentPlayer.loopSupported
onClicked: {
if (currentPlayer && currentPlayer.loopSupported) {
if (currentPlayer.loopState === MprisLoopState.None) {
currentPlayer.loopState = MprisLoopState.Track
} else if (currentPlayer.loopState === MprisLoopState.Track) {
currentPlayer.loopState = MprisLoopState.Playlist
} else {
currentPlayer.loopState = MprisLoopState.None
}
}
}
}
Text {
anchors.centerIn: parent
text: currentPlayer && currentPlayer.loopState === MprisLoopState.Track ? "repeat_one" : "repeat"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: repeatButton.enabled ?
(currentPlayer && currentPlayer.loopState !== MprisLoopState.None ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
}
}
}
}
}
}

View file

@ -0,0 +1,125 @@
import QtQuick
import "../../tabs" as Tabs
// Tab container with sliding animation
Item {
id: tabContainer
// Properties from parent
required property var shell
required property bool isRecording
required property var triggerMouseArea
property int currentTab: 0
// Signals to forward
signal recordingRequested()
signal stopRecordingRequested()
signal systemActionRequested(string action)
signal performanceActionRequested(string action)
// Hover detection combining all tab hovers
property bool isHovered: {
const tabHovers = [
mainDashboard.isHovered,
true, // Calendar tab should stay open when active
true, // Clipboard tab should stay open when active
true, // Notification tab should stay open when active
true, // Wallpaper tab should stay open when active
true, // Music tab should stay open when active
true // Settings tab should stay open when active
]
return tabHovers[currentTab] || false
}
// Track when text inputs have focus for keyboard management
property bool textInputFocused: currentTab === 6 && settingsTab.anyTextInputFocused
clip: true
// Sliding content container
Row {
id: slidingRow
width: parent.width * 7 // 7 tabs wide
height: parent.height
spacing: 0
// Animate horizontal position based on current tab
x: -tabContainer.currentTab * tabContainer.width
Behavior on x {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
// Tab 0: Main Dashboard
Tabs.MainDashboard {
id: mainDashboard
width: tabContainer.width
height: parent.height
shell: tabContainer.shell
isRecording: tabContainer.isRecording
triggerMouseArea: tabContainer.triggerMouseArea
onRecordingRequested: tabContainer.recordingRequested()
onStopRecordingRequested: tabContainer.stopRecordingRequested()
onSystemActionRequested: function(action) { tabContainer.systemActionRequested(action) }
onPerformanceActionRequested: function(action) { tabContainer.performanceActionRequested(action) }
}
// Tab 1: Calendar
Tabs.CalendarTab {
id: calendarTab
width: tabContainer.width
height: parent.height
shell: tabContainer.shell
isActive: tabContainer.currentTab === 1 || Math.abs(tabContainer.currentTab - 1) <= 1
}
// Tab 2: Clipboard
Tabs.ClipboardTab {
id: clipboardTab
width: tabContainer.width
height: parent.height
shell: tabContainer.shell
isActive: tabContainer.currentTab === 2 || Math.abs(tabContainer.currentTab - 2) <= 1
}
// Tab 3: Notifications
Tabs.NotificationTab {
id: notificationTab
width: tabContainer.width
height: parent.height
shell: tabContainer.shell
isActive: tabContainer.currentTab === 3 || Math.abs(tabContainer.currentTab - 3) <= 1
}
// Tab 4: Wallpapers
Tabs.WallpaperTab {
id: wallpaperTab
width: tabContainer.width
height: parent.height
isActive: tabContainer.currentTab === 4 || Math.abs(tabContainer.currentTab - 4) <= 1
}
// Tab 5: Music
Tabs.MusicTab {
id: musicTab
width: tabContainer.width
height: parent.height
shell: tabContainer.shell
isActive: tabContainer.currentTab === 5 || Math.abs(tabContainer.currentTab - 5) <= 1
}
// Tab 6: Settings
Tabs.SettingsTab {
id: settingsTab
width: tabContainer.width
height: parent.height
shell: tabContainer.shell
isActive: tabContainer.currentTab === 6 || Math.abs(tabContainer.currentTab - 6) <= 1
}
}
}

View file

@ -0,0 +1,132 @@
import QtQuick
import "root:/Data" as Data
// Tab navigation sidebar
Item {
id: tabNavigation
property int currentTab: 0
property var tabIcons: []
property bool containsMouse: sidebarMouseArea.containsMouse || tabColumn.containsMouse
MouseArea {
id: sidebarMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
}
// Tab button background - matches system controls
Rectangle {
width: 38
height: tabColumn.height + 12
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
radius: 19
border.width: 1
border.color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
// Subtle inner shadow effect
Rectangle {
anchors.fill: parent
anchors.margins: 1
color: Qt.darker(Data.ThemeManager.bgColor, 1.05)
radius: parent.radius - 1
opacity: 0.3
}
}
// Tab icon buttons
Column {
id: tabColumn
spacing: 6
anchors.top: parent.top
anchors.topMargin: 6
anchors.horizontalCenter: parent.horizontalCenter
property bool containsMouse: {
for (let i = 0; i < tabRepeater.count; i++) {
const tab = tabRepeater.itemAt(i)
if (tab && tab.mouseArea && tab.mouseArea.containsMouse) {
return true
}
}
return false
}
Repeater {
id: tabRepeater
model: 7
delegate: Rectangle {
width: 30
height: 30
radius: 15
// Dynamic background based on state
color: {
if (tabNavigation.currentTab === index) {
return Data.ThemeManager.accentColor
} else if (tabMouseArea.containsMouse) {
return Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.15)
} else {
return "transparent"
}
}
// Subtle shadow for active tab
Rectangle {
anchors.fill: parent
radius: parent.radius
color: "transparent"
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
border.width: tabNavigation.currentTab === index ? 0 : (tabMouseArea.containsMouse ? 1 : 0)
visible: tabNavigation.currentTab !== index
}
property alias mouseArea: tabMouseArea
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
tabNavigation.currentTab = index
}
}
Text {
anchors.centerIn: parent
text: tabNavigation.tabIcons[index] || ""
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: {
if (tabNavigation.currentTab === index) {
return Data.ThemeManager.bgColor
} else if (tabMouseArea.containsMouse) {
return Data.ThemeManager.accentColor
} else {
return Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
}
}
// Smooth color transitions
Behavior on color {
ColorAnimation { duration: 150 }
}
}
// Smooth transitions
Behavior on color {
ColorAnimation { duration: 150 }
}
// Subtle scale effect on hover
scale: tabMouseArea.containsMouse ? 1.05 : 1.0
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
}
}
}
}
}

View file

@ -0,0 +1,622 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
// Appearance settings content
Column {
width: parent.width
spacing: 20
// Theme Setting in Collapsible Section
SettingsCategory {
width: parent.width
title: "Theme Setting"
icon: "palette"
content: Component {
Column {
width: parent.width
spacing: 30 // Increased spacing between major sections
// Dark/Light Mode Switch
Column {
width: parent.width
spacing: 12
Text {
text: "Theme Mode"
color: Data.ThemeManager.fgColor
font.pixelSize: 15
font.bold: true
font.family: "Roboto"
}
Row {
spacing: 16
anchors.horizontalCenter: parent.horizontalCenter
Text {
text: "Light"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.family: "Roboto"
anchors.verticalCenter: parent.verticalCenter
}
// Toggle switch - enhanced design
Rectangle {
width: 64
height: 32
radius: 16
color: Data.ThemeManager.currentTheme.type === "dark" ?
Qt.lighter(Data.ThemeManager.accentColor, 0.8) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.2)
border.width: 2
border.color: Data.ThemeManager.currentTheme.type === "dark" ?
Data.ThemeManager.accentColor :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.4)
anchors.verticalCenter: parent.verticalCenter
// Inner track shadow
Rectangle {
anchors.fill: parent
anchors.margins: 2
radius: parent.radius - 2
color: "transparent"
border.width: 1
border.color: Qt.rgba(0, 0, 0, 0.1)
}
// Toggle handle
Rectangle {
id: toggleHandle
width: 26
height: 26
radius: 13
color: Data.ThemeManager.currentTheme.type === "dark" ?
Data.ThemeManager.bgColor : Data.ThemeManager.panelBackground
border.width: 2
border.color: Data.ThemeManager.currentTheme.type === "dark" ?
Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
anchors.verticalCenter: parent.verticalCenter
x: Data.ThemeManager.currentTheme.type === "dark" ? parent.width - width - 3 : 3
// Handle shadow
Rectangle {
anchors.centerIn: parent
anchors.verticalCenterOffset: 1
width: parent.width - 2
height: parent.height - 2
radius: parent.radius - 1
color: Qt.rgba(0, 0, 0, 0.1)
z: -1
}
// Handle highlight
Rectangle {
anchors.centerIn: parent
width: parent.width - 6
height: parent.height - 6
radius: parent.radius - 3
color: Qt.rgba(255, 255, 255, 0.15)
}
Behavior on x {
NumberAnimation {
duration: 250
easing.type: Easing.OutBack
easing.overshoot: 0.3
}
}
Behavior on border.color {
ColorAnimation { duration: 200 }
}
}
// Background color transition
Behavior on color {
ColorAnimation { duration: 200 }
}
Behavior on border.color {
ColorAnimation { duration: 200 }
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
console.log("Theme switch clicked, current:", Data.ThemeManager.currentThemeId)
var currentFamily = Data.ThemeManager.currentThemeId.replace(/_dark$|_light$/, "")
var newType = Data.ThemeManager.currentTheme.type === "dark" ? "light" : "dark"
var newThemeId = currentFamily + "_" + newType
console.log("Switching to:", newThemeId)
Data.ThemeManager.setTheme(newThemeId)
// Force update the settings if currentTheme isn't being saved properly
if (!Data.Settings.currentTheme) {
Data.Settings.currentTheme = newThemeId
Data.Settings.saveSettings()
}
}
onEntered: {
parent.scale = 1.05
}
onExited: {
parent.scale = 1.0
}
}
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
}
}
Text {
text: "Dark"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.family: "Roboto"
anchors.verticalCenter: parent.verticalCenter
}
}
}
// Separator
Rectangle {
width: parent.width - 40
height: 1
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
}
// Theme Selection
Column {
width: parent.width
spacing: 12
Text {
text: "Theme Family"
color: Data.ThemeManager.fgColor
font.pixelSize: 15
font.bold: true
font.family: "Roboto"
}
Text {
text: "Choose your preferred theme family"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
anchors.horizontalCenter: parent.horizontalCenter
}
// Compact 2x2 grid for themes
GridLayout {
columns: 2
columnSpacing: 8
rowSpacing: 8
anchors.horizontalCenter: parent.horizontalCenter
property var themeFamily: {
var currentFamily = Data.ThemeManager.currentThemeId.replace(/_dark$|_light$/, "")
return currentFamily
}
property var themeFamilies: [
{ id: "oxocarbon", name: "Oxocarbon", description: "IBM Carbon" },
{ id: "dracula", name: "Dracula", description: "Vibrant" },
{ id: "gruvbox", name: "Gruvbox", description: "Retro" },
{ id: "catppuccin", name: "Catppuccin", description: "Pastel" },
{ id: "matugen", name: "Matugen", description: "Generated" }
]
Repeater {
model: parent.themeFamilies
delegate: Rectangle {
Layout.preferredWidth: 140
Layout.preferredHeight: 50
radius: 10
color: parent.themeFamily === modelData.id ?
Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: parent.themeFamily === modelData.id ? 2 : 1
border.color: parent.themeFamily === modelData.id ?
Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 10
spacing: 6
// Compact theme preview colors
Row {
spacing: 1
property var previewTheme: Data.ThemeManager.themes[modelData.id + "_" + Data.ThemeManager.currentTheme.type] || Data.ThemeManager.themes[modelData.id + "_dark"]
Rectangle { width: 4; height: 14; radius: 1; color: parent.previewTheme.base00 }
Rectangle { width: 4; height: 14; radius: 1; color: parent.previewTheme.base0E }
Rectangle { width: 4; height: 14; radius: 1; color: parent.previewTheme.base0D }
Rectangle { width: 4; height: 14; radius: 1; color: parent.previewTheme.base0B }
}
Column {
spacing: 1
anchors.verticalCenter: parent.verticalCenter
Text {
text: modelData.name
color: parent.parent.parent.parent.themeFamily === modelData.id ?
Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
font.pixelSize: 12
font.bold: parent.parent.parent.parent.themeFamily === modelData.id
font.family: "Roboto"
}
Text {
text: modelData.description
color: parent.parent.parent.parent.themeFamily === modelData.id ?
Qt.rgba(Data.ThemeManager.bgColor.r, Data.ThemeManager.bgColor.g, Data.ThemeManager.bgColor.b, 0.8) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
font.pixelSize: 9
font.family: "Roboto"
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
var themeType = Data.ThemeManager.currentTheme.type
var newThemeId = modelData.id + "_" + themeType
console.log("Theme card clicked:", newThemeId)
Data.ThemeManager.setTheme(newThemeId)
}
onEntered: {
parent.scale = 1.02
}
onExited: {
parent.scale = 1.0
}
}
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
}
}
}
}
}
// Separator
Rectangle {
width: parent.width - 40
height: 1
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
}
// Accent Colors
Column {
width: parent.width
spacing: 12
Text {
text: "Accent Colors"
color: Data.ThemeManager.fgColor
font.pixelSize: 15
font.bold: true
font.family: "Roboto"
}
Text {
text: "Choose your preferred accent color for " + Data.ThemeManager.currentTheme.name
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
wrapMode: Text.Wrap
width: parent.width
anchors.horizontalCenter: parent.horizontalCenter
}
// Compact flow layout for accent colors
Flow {
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 20 // Margins to prevent clipping
spacing: 8
property var accentColors: {
var currentFamily = Data.ThemeManager.currentThemeId.replace(/_dark$|_light$/, "")
var themeColors = []
// Theme-specific accent colors - reduced to 5 per theme for compactness
if (currentFamily === "dracula") {
themeColors.push(
{ name: "Magenta", dark: "#ff79c6", light: "#e91e63" },
{ name: "Purple", dark: "#bd93f9", light: "#6c7ce0" },
{ name: "Cyan", dark: "#8be9fd", light: "#17a2b8" },
{ name: "Green", dark: "#50fa7b", light: "#27ae60" },
{ name: "Orange", dark: "#ffb86c", light: "#f39c12" }
)
} else if (currentFamily === "gruvbox") {
themeColors.push(
{ name: "Orange", dark: "#fe8019", light: "#d65d0e" },
{ name: "Red", dark: "#fb4934", light: "#cc241d" },
{ name: "Yellow", dark: "#fabd2f", light: "#d79921" },
{ name: "Green", dark: "#b8bb26", light: "#98971a" },
{ name: "Purple", dark: "#d3869b", light: "#b16286" }
)
} else if (currentFamily === "catppuccin") {
themeColors.push(
{ name: "Mauve", dark: "#cba6f7", light: "#8839ef" },
{ name: "Blue", dark: "#89b4fa", light: "#1e66f5" },
{ name: "Teal", dark: "#94e2d5", light: "#179299" },
{ name: "Green", dark: "#a6e3a1", light: "#40a02b" },
{ name: "Peach", dark: "#fab387", light: "#fe640b" }
)
} else if (currentFamily === "matugen") {
// Use dynamic matugen colors if available
if (Data.ThemeManager.matugen && Data.ThemeManager.matugen.isMatugenActive()) {
themeColors.push(
{ name: "Primary", dark: Data.ThemeManager.matugen.getMatugenColor("primary") || "#adc6ff", light: Data.ThemeManager.matugen.getMatugenColor("primary") || "#0f62fe" },
{ name: "Secondary", dark: Data.ThemeManager.matugen.getMatugenColor("secondary") || "#bfc6dc", light: Data.ThemeManager.matugen.getMatugenColor("secondary") || "#6272a4" },
{ name: "Tertiary", dark: Data.ThemeManager.matugen.getMatugenColor("tertiary") || "#debcdf", light: Data.ThemeManager.matugen.getMatugenColor("tertiary") || "#b16286" },
{ name: "Surface", dark: Data.ThemeManager.matugen.getMatugenColor("surface_tint") || "#adc6ff", light: Data.ThemeManager.matugen.getMatugenColor("surface_tint") || "#0f62fe" },
{ name: "Error", dark: Data.ThemeManager.matugen.getMatugenColor("error") || "#ffb4ab", light: Data.ThemeManager.matugen.getMatugenColor("error") || "#ba1a1a" }
)
} else {
// Fallback matugen colors
themeColors.push(
{ name: "Primary", dark: "#adc6ff", light: "#0f62fe" },
{ name: "Secondary", dark: "#bfc6dc", light: "#6272a4" },
{ name: "Tertiary", dark: "#debcdf", light: "#b16286" },
{ name: "Surface", dark: "#adc6ff", light: "#0f62fe" },
{ name: "Error", dark: "#ffb4ab", light: "#ba1a1a" }
)
}
} else { // oxocarbon and fallback
themeColors.push(
{ name: "Purple", dark: "#be95ff", light: "#8a3ffc" },
{ name: "Blue", dark: "#78a9ff", light: "#0f62fe" },
{ name: "Cyan", dark: "#3ddbd9", light: "#007d79" },
{ name: "Green", dark: "#42be65", light: "#198038" },
{ name: "Pink", dark: "#ff7eb6", light: "#d12771" }
)
}
return themeColors
}
Repeater {
model: parent.accentColors
delegate: Rectangle {
width: 60
height: 50
radius: 10
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: Data.ThemeManager.accentColor.toString() === (Data.ThemeManager.currentTheme.type === "dark" ? modelData.dark : modelData.light) ? 3 : 1
border.color: Data.ThemeManager.accentColor.toString() === (Data.ThemeManager.currentTheme.type === "dark" ? modelData.dark : modelData.light) ?
Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Column {
anchors.centerIn: parent
spacing: 4
Rectangle {
width: 20
height: 20
radius: 10
color: Data.ThemeManager.currentTheme.type === "dark" ? modelData.dark : modelData.light
anchors.horizontalCenter: parent.horizontalCenter
}
Text {
text: modelData.name
color: Data.ThemeManager.fgColor
font.pixelSize: 9
font.family: "Roboto"
font.bold: true
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
// Set custom accent
Data.Settings.useCustomAccent = true
Data.ThemeManager.setCustomAccent(modelData.dark, modelData.light)
}
onEntered: {
parent.scale = 1.05
}
onExited: {
parent.scale = 1.0
}
}
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
}
}
}
}
}
}
}
}
// Animation Settings in Collapsible Section
SettingsCategory {
width: parent.width
title: "Animation Settings"
icon: "animation"
content: Component {
Column {
width: parent.width
spacing: 20
Text {
text: "Configure workspace change animations"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
anchors.horizontalCenter: parent.horizontalCenter
}
// Workspace Burst Toggle
Row {
width: parent.width
height: 40
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 80
Text {
text: "Workspace Burst Effect"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
}
Text {
text: "Expanding rings when switching workspaces"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
font.pixelSize: 11
font.family: "Roboto"
}
}
// Toggle switch for burst
Rectangle {
width: 50
height: 25
radius: 12.5
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
color: Data.Settings.workspaceBurstEnabled ?
Qt.lighter(Data.ThemeManager.accentColor, 0.8) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.2)
border.width: 1
border.color: Data.Settings.workspaceBurstEnabled ?
Data.ThemeManager.accentColor :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.4)
Rectangle {
width: 20
height: 20
radius: 10
color: Data.ThemeManager.bgColor
border.width: 1.5
border.color: Data.Settings.workspaceBurstEnabled ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
anchors.verticalCenter: parent.verticalCenter
x: Data.Settings.workspaceBurstEnabled ? parent.width - width - 2.5 : 2.5
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutQuad
}
}
}
Behavior on color { ColorAnimation { duration: 200 } }
Behavior on border.color { ColorAnimation { duration: 200 } }
MouseArea {
anchors.fill: parent
onClicked: {
Data.Settings.workspaceBurstEnabled = !Data.Settings.workspaceBurstEnabled
}
}
}
}
// Workspace Glow Toggle
Row {
width: parent.width
height: 40
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 80
Text {
text: "Workspace Shadow Glow"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
}
Text {
text: "Accent color glow in workspace shadow"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
font.pixelSize: 11
font.family: "Roboto"
}
}
// Toggle switch for glow
Rectangle {
width: 50
height: 25
radius: 12.5
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
color: Data.Settings.workspaceGlowEnabled ?
Qt.lighter(Data.ThemeManager.accentColor, 0.8) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.2)
border.width: 1
border.color: Data.Settings.workspaceGlowEnabled ?
Data.ThemeManager.accentColor :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.4)
Rectangle {
width: 20
height: 20
radius: 10
color: Data.ThemeManager.bgColor
border.width: 1.5
border.color: Data.Settings.workspaceGlowEnabled ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
anchors.verticalCenter: parent.verticalCenter
x: Data.Settings.workspaceGlowEnabled ? parent.width - width - 2.5 : 2.5
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutQuad
}
}
}
Behavior on color { ColorAnimation { duration: 200 } }
Behavior on border.color { ColorAnimation { duration: 200 } }
MouseArea {
anchors.fill: parent
onClicked: {
Data.Settings.workspaceGlowEnabled = !Data.Settings.workspaceGlowEnabled
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,121 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// Music Player settings content
Column {
width: parent.width
spacing: 20
// Auto-switch to active player
Column {
width: parent.width
spacing: 12
Text {
text: "Auto-switch to Active Player"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Text {
text: "Automatically switch to the player that starts playing music"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
wrapMode: Text.Wrap
width: parent.width
}
Rectangle {
width: 200
height: 35
radius: 18
color: Data.Settings.autoSwitchPlayer ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: 1
border.color: Data.ThemeManager.accentColor
Behavior on color {
ColorAnimation { duration: 200 }
}
Text {
anchors.centerIn: parent
text: Data.Settings.autoSwitchPlayer ? "Enabled" : "Disabled"
color: Data.Settings.autoSwitchPlayer ? Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
Behavior on color {
ColorAnimation { duration: 200 }
}
}
MouseArea {
anchors.fill: parent
onClicked: {
Data.Settings.autoSwitchPlayer = !Data.Settings.autoSwitchPlayer
}
}
}
}
// Always show player dropdown
Column {
width: parent.width
spacing: 12
Text {
text: "Always Show Player Dropdown"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Text {
text: "Show the player selection dropdown even with only one player"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
wrapMode: Text.Wrap
width: parent.width
}
Rectangle {
width: 200
height: 35
radius: 18
color: Data.Settings.alwaysShowPlayerDropdown ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: 1
border.color: Data.ThemeManager.accentColor
Behavior on color {
ColorAnimation { duration: 200 }
}
Text {
anchors.centerIn: parent
text: Data.Settings.alwaysShowPlayerDropdown ? "Enabled" : "Disabled"
color: Data.Settings.alwaysShowPlayerDropdown ? Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
Behavior on color {
ColorAnimation { duration: 200 }
}
}
MouseArea {
anchors.fill: parent
onClicked: {
Data.Settings.alwaysShowPlayerDropdown = !Data.Settings.alwaysShowPlayerDropdown
}
}
}
}
}

View file

@ -0,0 +1,517 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
// Night Light settings content
Item {
id: nightLightSettings
width: parent.width
height: contentColumn.height
Column {
id: contentColumn
width: parent.width
spacing: 20
// Night Light Enable Toggle
Row {
width: parent.width
spacing: 16
Column {
width: parent.width - nightLightToggle.width - 16
spacing: 4
Text {
text: "Enable Night Light"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Text {
text: "Reduces blue light to help protect your eyes and improve sleep"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
wrapMode: Text.Wrap
}
}
Rectangle {
id: nightLightToggle
width: 50
height: 28
radius: 14
color: Data.Settings.nightLightEnabled ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Behavior on color {
ColorAnimation { duration: 200 }
}
Rectangle {
width: 20
height: 20
radius: 10
color: Data.ThemeManager.bgColor
x: Data.Settings.nightLightEnabled ? parent.width - width - 4 : 4
anchors.verticalCenter: parent.verticalCenter
Behavior on x {
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
Data.Settings.nightLightEnabled = !Data.Settings.nightLightEnabled
}
onEntered: {
parent.scale = 1.05
}
onExited: {
parent.scale = 1.0
}
}
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
}
}
}
// Warmth Level Slider
Column {
width: parent.width
spacing: 12
Text {
text: "Warmth Level"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Text {
text: "Adjust how warm the screen filter appears"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
wrapMode: Text.Wrap
width: parent.width
}
Row {
width: parent.width
spacing: 12
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Cool"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
font.pixelSize: 12
font.family: "Roboto"
}
Slider {
id: warmthSlider
width: parent.width - 120
height: 30
from: 0.1
to: 1.0
value: Data.Settings.nightLightWarmth || 0.4
stepSize: 0.1
onValueChanged: {
Data.Settings.nightLightWarmth = value
}
background: Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 6
radius: 3
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.2)
Rectangle {
width: warmthSlider.visualPosition * parent.width
height: parent.height
radius: parent.radius
color: Qt.rgba(1.0, 0.8 - warmthSlider.value * 0.3, 0.4, 1.0)
}
}
handle: Rectangle {
x: warmthSlider.leftPadding + warmthSlider.visualPosition * (warmthSlider.availableWidth - width)
y: warmthSlider.topPadding + warmthSlider.availableHeight / 2 - height / 2
width: 20
height: 20
radius: 10
color: Data.ThemeManager.accentColor
border.color: Qt.lighter(Data.ThemeManager.accentColor, 1.2)
border.width: 2
}
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Warm"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
font.pixelSize: 12
font.family: "Roboto"
}
}
}
// Auto-enable Toggle
Row {
width: parent.width
spacing: 16
Column {
width: parent.width - autoToggle.width - 16
spacing: 4
Text {
text: "Auto-enable Schedule"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Text {
text: "Automatically turn on night light at sunset/bedtime"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
wrapMode: Text.Wrap
}
}
Rectangle {
id: autoToggle
width: 50
height: 28
radius: 14
color: Data.Settings.nightLightAuto ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Behavior on color {
ColorAnimation { duration: 200 }
}
Rectangle {
width: 20
height: 20
radius: 10
color: Data.ThemeManager.bgColor
x: Data.Settings.nightLightAuto ? parent.width - width - 4 : 4
anchors.verticalCenter: parent.verticalCenter
Behavior on x {
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
Data.Settings.nightLightAuto = !Data.Settings.nightLightAuto
}
onEntered: {
parent.scale = 1.05
}
onExited: {
parent.scale = 1.0
}
}
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
}
}
}
// Schedule Time Controls - visible when auto-enable is on
Column {
width: parent.width
spacing: 16
visible: Data.Settings.nightLightAuto
opacity: Data.Settings.nightLightAuto ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation { duration: 200 }
}
Text {
text: "Schedule Times"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
// Start and End Time Row
Row {
width: parent.width
spacing: 20
// Start Time
Column {
id: startTimeColumn
width: (parent.width - parent.spacing) / 2
spacing: 8
Text {
text: "Start Time"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
}
Text {
text: "Night light turns on"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 12
font.family: "Roboto"
}
Rectangle {
id: startTimeButton
width: parent.width
height: 40
radius: 8
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: 1
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Row {
anchors.centerIn: parent
spacing: 8
Text {
text: (Data.Settings.nightLightStartHour || 20).toString().padStart(2, '0') + ":00"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
onClicked: {
startTimePopup.open()
}
}
}
// Start Time Popup
Popup {
id: startTimePopup
width: startTimeButton.width
height: 170
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
y: startTimeButton.y - height - 10
x: startTimeButton.x
dim: false
background: Rectangle {
color: Data.ThemeManager.bgColor
radius: 12
border.width: 2
border.color: Data.ThemeManager.accentColor
}
Column {
anchors.centerIn: parent
spacing: 12
Text {
text: "Select Start Hour"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
anchors.horizontalCenter: parent.horizontalCenter
}
GridLayout {
columns: 6
columnSpacing: 6
rowSpacing: 6
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: 24
delegate: Rectangle {
width: 24
height: 24
radius: 4
color: (Data.Settings.nightLightStartHour || 20) === modelData ?
Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: 1
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Text {
anchors.centerIn: parent
text: modelData.toString().padStart(2, '0')
color: (Data.Settings.nightLightStartHour || 20) === modelData ?
Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
font.pixelSize: 10
font.bold: true
font.family: "Roboto"
}
MouseArea {
anchors.fill: parent
onClicked: {
Data.Settings.nightLightStartHour = modelData
startTimePopup.close()
}
}
}
}
}
}
}
}
// End Time
Column {
id: endTimeColumn
width: (parent.width - parent.spacing) / 2
spacing: 8
Text {
text: "End Time"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
}
Text {
text: "Night light turns off"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 12
font.family: "Roboto"
}
Rectangle {
id: endTimeButton
width: parent.width
height: 40
radius: 8
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: 1
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Row {
anchors.centerIn: parent
spacing: 8
Text {
text: (Data.Settings.nightLightEndHour || 6).toString().padStart(2, '0') + ":00"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
onClicked: {
endTimePopup.open()
}
}
}
// End Time Popup
Popup {
id: endTimePopup
width: endTimeButton.width
height: 170
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
y: endTimeButton.y - height - 10
x: endTimeButton.x
dim: false
background: Rectangle {
color: Data.ThemeManager.bgColor
radius: 12
border.width: 2
border.color: Data.ThemeManager.accentColor
}
Column {
anchors.centerIn: parent
spacing: 12
Text {
text: "Select End Hour"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
anchors.horizontalCenter: parent.horizontalCenter
}
GridLayout {
columns: 6
columnSpacing: 6
rowSpacing: 6
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: 24
delegate: Rectangle {
width: 24
height: 24
radius: 4
color: (Data.Settings.nightLightEndHour || 6) === modelData ?
Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: 1
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Text {
anchors.centerIn: parent
text: modelData.toString().padStart(2, '0')
color: (Data.Settings.nightLightEndHour || 6) === modelData ?
Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
font.pixelSize: 10
font.bold: true
font.family: "Roboto"
}
MouseArea {
anchors.fill: parent
onClicked: {
Data.Settings.nightLightEndHour = modelData
endTimePopup.close()
}
}
}
}
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,531 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// Notification settings content
Item {
id: notificationSettings
width: parent.width
height: contentColumn.height
// Expose the text input focus for parent keyboard management
property bool anyTextInputFocused: appNameInput.activeFocus
Column {
id: contentColumn
width: parent.width
spacing: 20
// Display Time Setting
Column {
width: parent.width
spacing: 12
Text {
text: "Display Time"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Text {
text: "How long notifications stay visible on screen"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
wrapMode: Text.Wrap
width: parent.width
}
Row {
spacing: 16
width: parent.width
Slider {
id: displayTimeSlider
width: parent.width - timeLabel.width - 16
height: 30
from: 2000
to: 15000
stepSize: 1000
value: Data.Settings.displayTime
onValueChanged: {
Data.Settings.displayTime = value
}
background: Rectangle {
width: displayTimeSlider.availableWidth
height: 6
radius: 3
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: displayTimeSlider.visualPosition * parent.width
height: parent.height
radius: parent.radius
color: Data.ThemeManager.accentColor
}
}
handle: Rectangle {
x: displayTimeSlider.leftPadding + displayTimeSlider.visualPosition * (displayTimeSlider.availableWidth - width)
y: displayTimeSlider.topPadding + displayTimeSlider.availableHeight / 2 - height / 2
width: 20
height: 20
radius: 10
color: Data.ThemeManager.accentColor
border.color: Qt.lighter(Data.ThemeManager.accentColor, 1.2)
border.width: 2
scale: displayTimeSlider.pressed ? 1.2 : 1.0
Behavior on scale {
NumberAnimation { duration: 150 }
}
}
}
Text {
id: timeLabel
text: (displayTimeSlider.value / 1000).toFixed(1) + "s"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.family: "Roboto"
anchors.verticalCenter: parent.verticalCenter
width: 40
}
}
}
// Max History Items
Column {
width: parent.width
spacing: 12
Text {
text: "History Limit"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Text {
text: "Maximum number of notifications to keep in history"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
wrapMode: Text.Wrap
width: parent.width
}
Row {
spacing: 16
width: parent.width
Slider {
id: historySlider
width: parent.width - historyLabel.width - 16
height: 30
from: 10
to: 100
stepSize: 5
value: Data.Settings.historyLimit
onValueChanged: {
Data.Settings.historyLimit = value
}
background: Rectangle {
width: historySlider.availableWidth
height: 6
radius: 3
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: historySlider.visualPosition * parent.width
height: parent.height
radius: parent.radius
color: Data.ThemeManager.accentColor
}
}
handle: Rectangle {
x: historySlider.leftPadding + historySlider.visualPosition * (historySlider.availableWidth - width)
y: historySlider.topPadding + historySlider.availableHeight / 2 - height / 2
width: 20
height: 20
radius: 10
color: Data.ThemeManager.accentColor
border.color: Qt.lighter(Data.ThemeManager.accentColor, 1.2)
border.width: 2
scale: historySlider.pressed ? 1.2 : 1.0
Behavior on scale {
NumberAnimation { duration: 150 }
}
}
}
Text {
id: historyLabel
text: historySlider.value + " items"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.family: "Roboto"
anchors.verticalCenter: parent.verticalCenter
width: 60
}
}
}
// Ignored Apps Setting
Column {
width: parent.width
spacing: 12
Text {
text: "Ignored Applications"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Text {
text: "Applications that won't show notifications"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
wrapMode: Text.Wrap
width: parent.width
}
// Current ignored apps list
Rectangle {
width: parent.width
height: Math.max(100, ignoredAppsFlow.height + 16)
radius: 12
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: 1
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Flow {
id: ignoredAppsFlow
anchors.fill: parent
anchors.margins: 8
spacing: 6
Repeater {
model: Data.Settings.ignoredApps
delegate: Rectangle {
width: appNameText.width + removeButton.width + 16
height: 28
radius: 14
color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.15)
border.width: 1
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
Row {
anchors.centerIn: parent
spacing: 4
Text {
id: appNameText
anchors.verticalCenter: parent.verticalCenter
text: modelData
color: Data.ThemeManager.fgColor
font.pixelSize: 12
font.family: "Roboto"
}
Rectangle {
id: removeButton
width: 18
height: 18
radius: 9
color: removeMouseArea.containsMouse ?
Qt.rgba(1, 0.3, 0.3, 0.8) : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.5)
Behavior on color {
ColorAnimation { duration: 150 }
}
Text {
anchors.centerIn: parent
text: "×"
color: "white"
font.pixelSize: 12
font.bold: true
}
MouseArea {
id: removeMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Data.Settings.removeIgnoredApp(modelData)
}
}
}
}
}
}
// Add new app button
Rectangle {
width: addAppText.width + 36
height: 28
radius: 14
color: addAppMouseArea.containsMouse ?
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) :
Qt.lighter(Data.ThemeManager.bgColor, 1.2)
border.width: 2
border.color: Data.ThemeManager.accentColor
Behavior on color {
ColorAnimation { duration: 150 }
}
Row {
anchors.centerIn: parent
spacing: 6
Text {
anchors.verticalCenter: parent.verticalCenter
text: "add"
font.family: "Material Symbols Outlined"
font.pixelSize: 14
color: Data.ThemeManager.accentColor
}
Text {
id: addAppText
anchors.verticalCenter: parent.verticalCenter
text: "Add App"
color: Data.ThemeManager.accentColor
font.pixelSize: 12
font.bold: true
font.family: "Roboto"
}
}
MouseArea {
id: addAppMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: addAppPopup.open()
}
}
}
}
// Quick suggestions
Column {
width: parent.width
spacing: 8
Text {
text: "Common Apps"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 12
font.family: "Roboto"
}
Flow {
width: parent.width
spacing: 6
Repeater {
model: ["Discord", "Spotify", "Steam", "Firefox", "Chrome", "VSCode", "Slack"]
delegate: Rectangle {
width: suggestedAppText.width + 16
height: 24
radius: 12
color: suggestionMouseArea.containsMouse ?
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.1) :
"transparent"
border.width: 1
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Text {
id: suggestedAppText
anchors.centerIn: parent
text: modelData
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 11
font.family: "Roboto"
}
MouseArea {
id: suggestionMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Data.Settings.addIgnoredApp(modelData)
}
}
}
}
}
}
}
}
// Add app popup
Popup {
id: addAppPopup
parent: notificationSettings
width: 280
height: 160
x: (parent.width - width) / 2
y: (parent.height - height) / 2
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: Data.ThemeManager.bgColor
border.color: Data.ThemeManager.accentColor
border.width: 2
radius: 20
}
Column {
anchors.centerIn: parent
spacing: 16
width: parent.width - 40
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "Add Ignored App"
color: Data.ThemeManager.accentColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Rectangle {
width: parent.width
height: 40
radius: 20
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: appNameInput.activeFocus ? 2 : 1
border.color: appNameInput.activeFocus ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Behavior on border.color {
ColorAnimation { duration: 150 }
}
TextInput {
id: appNameInput
anchors.fill: parent
anchors.margins: 12
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.family: "Roboto"
selectByMouse: true
clip: true
verticalAlignment: TextInput.AlignVCenter
focus: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
addAppButton.clicked()
event.accepted = true
}
}
// Placeholder text implementation
Text {
anchors.fill: parent
anchors.margins: 12
text: "App name (e.g. Discord)"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.5)
font.pixelSize: 14
font.family: "Roboto"
verticalAlignment: Text.AlignVCenter
visible: appNameInput.text === ""
}
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 12
Rectangle {
width: 80
height: 32
radius: 16
color: cancelMouseArea.containsMouse ? Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.1) : "transparent"
border.width: 1
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Text {
anchors.centerIn: parent
text: "Cancel"
color: Data.ThemeManager.fgColor
font.pixelSize: 12
font.family: "Roboto"
}
MouseArea {
id: cancelMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
appNameInput.text = ""
addAppPopup.close()
}
}
}
Rectangle {
id: addAppButton
width: 80
height: 32
radius: 16
color: addMouseArea.containsMouse ? Qt.lighter(Data.ThemeManager.accentColor, 1.1) : Data.ThemeManager.accentColor
signal clicked()
onClicked: {
if (appNameInput.text.trim() !== "") {
if (Data.Settings.addIgnoredApp(appNameInput.text.trim())) {
appNameInput.text = ""
addAppPopup.close()
}
}
}
Text {
anchors.centerIn: parent
text: "Add"
color: Data.ThemeManager.bgColor
font.pixelSize: 12
font.bold: true
font.family: "Roboto"
}
MouseArea {
id: addMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: parent.clicked()
}
}
}
}
onOpened: {
appNameInput.forceActiveFocus()
}
}
}

View file

@ -0,0 +1,104 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// Reusable collapsible settings category component
Item {
id: categoryRoot
property string title: ""
property string icon: ""
property bool expanded: false
property alias content: contentLoader.sourceComponent
height: headerRect.height + (expanded ? contentLoader.height + 20 : 0)
Behavior on height {
NumberAnimation {
duration: 250
easing.type: Easing.OutCubic
}
}
// Category header
Rectangle {
id: headerRect
width: parent.width
height: 50
radius: 12
color: expanded ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.1) :
Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: expanded ? 2 : 1
border.color: expanded ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 16
spacing: 12
Text {
anchors.verticalCenter: parent.verticalCenter
text: categoryRoot.icon
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: expanded ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: categoryRoot.title
color: expanded ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
}
// Expand/collapse arrow
Text {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: 16
text: expanded ? "expand_less" : "expand_more"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: expanded ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
Behavior on rotation {
NumberAnimation {
duration: 250
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
onClicked: {
categoryRoot.expanded = !categoryRoot.expanded
}
}
}
// Category content
Loader {
id: contentLoader
anchors.top: headerRect.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: expanded ? 20 : 0
anchors.leftMargin: 16
anchors.rightMargin: 16
visible: expanded
opacity: expanded ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 250
easing.type: Easing.OutCubic
}
}
}
}

View file

@ -0,0 +1,131 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// System settings content
Item {
id: systemSettings
width: parent.width
height: contentColumn.height
// Expose the text input focus for parent keyboard management
property bool anyTextInputFocused: videoPathInput.activeFocus || wallpaperDirectoryInput.activeFocus
Column {
id: contentColumn
width: parent.width
spacing: 20
// Video Recording Path
Column {
width: parent.width
spacing: 8
Text {
text: "Video Recording Path"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Rectangle {
width: parent.width
height: 40
radius: 8
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: videoPathInput.activeFocus ? 2 : 1
border.color: videoPathInput.activeFocus ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Behavior on border.color {
ColorAnimation { duration: 150 }
}
TextInput {
id: videoPathInput
anchors.fill: parent
anchors.margins: 12
text: Data.Settings.videoPath
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.family: "Roboto"
selectByMouse: true
clip: true
verticalAlignment: TextInput.AlignVCenter
focus: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
onTextChanged: {
Data.Settings.videoPath = text
}
Keys.onPressed: function(event) {
// Allow default text input behavior
}
}
MouseArea {
anchors.fill: parent
onClicked: {
videoPathInput.forceActiveFocus()
}
}
}
}
// Wallpaper Directory
Column {
width: parent.width
spacing: 8
Text {
text: "Wallpaper Directory"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Rectangle {
width: parent.width
height: 40
radius: 8
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: wallpaperDirectoryInput.activeFocus ? 2 : 1
border.color: wallpaperDirectoryInput.activeFocus ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Behavior on border.color {
ColorAnimation { duration: 150 }
}
TextInput {
id: wallpaperDirectoryInput
anchors.fill: parent
anchors.margins: 12
text: Data.Settings.wallpaperDirectory
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.family: "Roboto"
selectByMouse: true
clip: true
verticalAlignment: TextInput.AlignVCenter
focus: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
onTextChanged: {
Data.Settings.wallpaperDirectory = text
}
MouseArea {
anchors.fill: parent
onClicked: {
wallpaperDirectoryInput.forceActiveFocus()
}
}
}
}
}
}
}

View file

@ -0,0 +1,197 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// Weather settings content
Item {
id: weatherSettings
width: parent.width
height: contentColumn.height
required property var shell
// Expose the text input focus for parent keyboard management
property bool anyTextInputFocused: locationInput.activeFocus
Column {
id: contentColumn
width: parent.width
spacing: 20
// Location Setting
Column {
width: parent.width
spacing: 8
Text {
text: "Location"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Row {
width: parent.width
spacing: 12
Rectangle {
width: parent.width - applyButton.width - 12
height: 40
radius: 8
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: locationInput.activeFocus ? 2 : 1
border.color: locationInput.activeFocus ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Behavior on border.color {
ColorAnimation { duration: 150 }
}
TextInput {
id: locationInput
anchors.fill: parent
anchors.margins: 12
text: Data.Settings.weatherLocation
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.family: "Roboto"
selectByMouse: true
clip: true
verticalAlignment: TextInput.AlignVCenter
focus: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
applyButton.clicked()
event.accepted = true
}
}
MouseArea {
anchors.fill: parent
onClicked: {
locationInput.forceActiveFocus()
}
}
}
}
Rectangle {
id: applyButton
width: 80
height: 40
radius: 8
color: applyMouseArea.containsMouse ? Qt.lighter(Data.ThemeManager.accentColor, 1.1) : Data.ThemeManager.accentColor
signal clicked()
onClicked: {
Data.Settings.weatherLocation = locationInput.text
weatherSettings.shell.weatherService.loadWeather()
}
Text {
anchors.centerIn: parent
text: "Apply"
color: Data.ThemeManager.bgColor
font.pixelSize: 12
font.bold: true
font.family: "Roboto"
}
MouseArea {
id: applyMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: parent.clicked()
}
}
}
}
// Temperature Units
Column {
width: parent.width
spacing: 12
Text {
text: "Temperature Units"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Row {
spacing: 12
Rectangle {
width: 80
height: 35
radius: 18
color: !Data.Settings.useFahrenheit ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: 1
border.color: Data.ThemeManager.accentColor
Behavior on color {
ColorAnimation { duration: 200 }
}
Text {
anchors.centerIn: parent
text: "°C"
color: !Data.Settings.useFahrenheit ? Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
Behavior on color {
ColorAnimation { duration: 200 }
}
}
MouseArea {
anchors.fill: parent
onClicked: {
Data.Settings.useFahrenheit = false
}
}
}
Rectangle {
width: 80
height: 35
radius: 18
color: Data.Settings.useFahrenheit ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: 1
border.color: Data.ThemeManager.accentColor
Behavior on color {
ColorAnimation { duration: 200 }
}
Text {
anchors.centerIn: parent
text: "°F"
color: Data.Settings.useFahrenheit ? Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
Behavior on color {
ColorAnimation { duration: 200 }
}
}
MouseArea {
anchors.fill: parent
onClicked: {
Data.Settings.useFahrenheit = true
}
}
}
}
}
}
}

View file

@ -0,0 +1,98 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
// Dual-button notification and clipboard history toggle bar
Rectangle {
id: root
width: 42
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 12
z: 2 // Above notification history overlay
required property bool notificationHistoryVisible
required property bool clipboardHistoryVisible
required property var notificationHistory
signal notificationToggleRequested()
signal clipboardToggleRequested()
// Combined hover state for parent component tracking
property bool containsMouse: notifButtonMouseArea.containsMouse || clipButtonMouseArea.containsMouse
property real buttonHeight: 38
height: buttonHeight * 2 + 4 // Two buttons with spacing
Item {
anchors.fill: parent
anchors.margins: 2
// Notifications toggle button (top half)
Rectangle {
id: notificationPill
anchors {
top: parent.top
left: parent.left
right: parent.right
bottom: parent.verticalCenter
bottomMargin: 2 // Half of button spacing
}
radius: 12
color: notifButtonMouseArea.containsMouse || root.notificationHistoryVisible ?
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.05)
border.color: notifButtonMouseArea.containsMouse || root.notificationHistoryVisible ? Data.ThemeManager.accentColor : "transparent"
border.width: 1
MouseArea {
id: notifButtonMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: root.notificationToggleRequested()
}
Label {
anchors.centerIn: parent
text: "notifications"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: notifButtonMouseArea.containsMouse || root.notificationHistoryVisible ?
Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
}
// Clipboard toggle button (bottom half)
Rectangle {
id: clipboardPill
anchors {
top: parent.verticalCenter
left: parent.left
right: parent.right
bottom: parent.bottom
topMargin: 2 // Half of button spacing
}
radius: 12
color: clipButtonMouseArea.containsMouse || root.clipboardHistoryVisible ?
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.05)
border.color: clipButtonMouseArea.containsMouse || root.clipboardHistoryVisible ? Data.ThemeManager.accentColor : "transparent"
border.width: 1
MouseArea {
id: clipButtonMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: root.clipboardToggleRequested()
}
Label {
anchors.centerIn: parent
text: "content_paste"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: clipButtonMouseArea.containsMouse || root.clipboardHistoryVisible ?
Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
}
}
}

View file

@ -0,0 +1,52 @@
import QtQuick
// Top-edge hover trigger
Rectangle {
id: root
width: 360
height: 1
color: "red"
anchors.top: parent.top
signal triggered()
// Hover detection area at screen top edge
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
property bool isHovered: containsMouse
// Timer coordination
onIsHoveredChanged: {
if (isHovered) {
showTimer.start()
hideTimer.stop()
} else {
hideTimer.start()
showTimer.stop()
}
}
onEntered: hideTimer.stop()
}
// Delayed show trigger to prevent accidental activation
Timer {
id: showTimer
interval: 200
onTriggered: root.triggered()
}
// Hide delay timer (controlled by parent)
Timer {
id: hideTimer
interval: 500
}
// Public interface
readonly property alias containsMouse: mouseArea.containsMouse
function stopHideTimer() { hideTimer.stop() }
function startHideTimer() { hideTimer.start() }
}

View file

@ -0,0 +1,226 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import "root:/Data" as Data
// System tray context menu
Rectangle {
id: root
width: parent.width
height: visible ? calculatedHeight : 0
visible: false
enabled: visible
clip: true
color: Data.ThemeManager.bgColor
border.color: Data.ThemeManager.accentColor
border.width: 2
radius: 20
required property var menu
required property var systemTrayY
required property var systemTrayHeight
property bool containsMouse: trayMenuMouseArea.containsMouse
property bool menuJustOpened: false
property point triggerPoint: Qt.point(0, 0)
property Item originalParent
property int totalCount: opener.children ? opener.children.values.length : 0
signal hideRequested()
MouseArea {
id: trayMenuMouseArea
anchors.fill: parent
hoverEnabled: true
preventStealing: true
propagateComposedEvents: false
}
onVisibleChanged: {
if (visible) {
menuJustOpened = true
Qt.callLater(function() {
menuJustOpened = false
})
}
}
function toggle() {
visible = !visible
if (visible) {
menuJustOpened = true
Qt.callLater(function() {
menuJustOpened = false
})
}
}
function show(point, parentItem) {
visible = true
menuJustOpened = true
Qt.callLater(function() {
menuJustOpened = false
})
}
function hide() {
visible = false
menuJustOpened = false
// Small delay before notifying hide to prevent control panel flicker
Qt.callLater(function() {
hideRequested()
})
}
// Smart positioning to avoid screen edges
y: {
var preferredY = systemTrayY + systemTrayHeight + 10
var availableSpace = parent.height - preferredY - 20
if (calculatedHeight > availableSpace) {
return systemTrayY - height - 10
}
return preferredY
}
Behavior on height {
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
}
// Dynamic height calculation based on menu item count and types
property int calculatedHeight: {
if (totalCount === 0) return 40
var separatorCount = 0
var regularItemCount = 0
if (opener.children && opener.children.values) {
for (var i = 0; i < opener.children.values.length; i++) {
if (opener.children.values[i].isSeparator) {
separatorCount++
} else {
regularItemCount++
}
}
}
// Calculate total height: separators + grid rows + margins
var separatorHeight = separatorCount * 12
var regularItemRows = Math.ceil(regularItemCount / 2)
var regularItemHeight = regularItemRows * 32
return Math.max(80, 35 + separatorHeight + regularItemHeight + 40)
}
// Menu opener handles the native menu integration
QsMenuOpener {
id: opener
menu: root.menu
}
// Grid layout for menu items (2 columns)
GridView {
id: gridView
anchors.fill: parent
anchors.margins: 20
cellWidth: width / 2
cellHeight: 32
interactive: false
flow: GridView.FlowLeftToRight
layoutDirection: Qt.LeftToRight
model: ScriptModel {
values: opener.children ? [...opener.children.values] : []
}
delegate: Item {
id: entry
required property var modelData
required property int index
width: gridView.cellWidth - 4
height: modelData.isSeparator ? 12 : 30
// Separator line
Rectangle {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
anchors.topMargin: 4
anchors.bottomMargin: 4
visible: modelData.isSeparator
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width * 0.8
height: 1
color: Qt.darker(Data.ThemeManager.accentColor, 1.5)
opacity: 0.6
}
}
// Regular menu item
Rectangle {
id: itemBackground
anchors.fill: parent
anchors.margins: 2
visible: !modelData.isSeparator
color: "transparent"
radius: 6
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 6
Image {
Layout.preferredWidth: 16
Layout.preferredHeight: 16
source: modelData?.icon ?? ""
visible: (modelData?.icon ?? "") !== ""
fillMode: Image.PreserveAspectFit
}
Text {
Layout.fillWidth: true
color: mouseArea.containsMouse ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
text: modelData?.text ?? ""
font.pixelSize: 11
font.family: "Roboto"
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
maximumLineCount: 1
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
enabled: (modelData?.enabled ?? true) && root.visible && !modelData.isSeparator
onEntered: itemBackground.color = Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.15)
onExited: itemBackground.color = "transparent"
onClicked: {
modelData.triggered()
root.hide()
}
}
}
}
}
// Empty state indicator
Item {
anchors.centerIn: gridView
visible: gridView.count === 0
Label {
anchors.centerIn: parent
text: "No tray items available"
color: Qt.darker(Data.ThemeManager.fgColor, 2)
font.pixelSize: 14
font.family: "Roboto"
}
}
}

View file

@ -0,0 +1,149 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import "root:/Data" as Data
// Wallpaper selector grid
Item {
id: root
property bool isVisible: false
signal visibilityChanged(bool visible)
// Use all space provided by parent
anchors.fill: parent
visible: isVisible
enabled: visible
clip: true
property bool containsMouse: wallpaperSelectorMouseArea.containsMouse || scrollView.containsMouse
property bool menuJustOpened: false
// Hover state management for auto-hide functionality
onContainsMouseChanged: {
if (containsMouse) {
hideTimer.stop()
} else if (!menuJustOpened && !isVisible) {
hideTimer.restart()
}
}
onVisibleChanged: {
if (visible) {
menuJustOpened = true
hideTimer.stop()
Qt.callLater(function() {
menuJustOpened = false
})
}
}
MouseArea {
id: wallpaperSelectorMouseArea
anchors.fill: parent
hoverEnabled: true
preventStealing: false
propagateComposedEvents: true
}
// Scrollable wallpaper grid with memory-conscious loading
ScrollView {
id: scrollView
anchors.fill: parent
clip: true
property bool containsMouse: gridMouseArea.containsMouse
MouseArea {
id: gridMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
}
GridView {
id: wallpaperGrid
anchors.fill: parent
cellWidth: parent.width / 2 - 8 // 2-column layout with spacing
cellHeight: cellWidth * 0.6 // Wallpaper aspect ratio
model: Data.WallpaperManager.wallpaperList
cacheBuffer: 0 // Memory optimization - no cache buffer
leftMargin: 4
rightMargin: 4
topMargin: 4
bottomMargin: 4
delegate: Item {
width: wallpaperGrid.cellWidth - 8
height: wallpaperGrid.cellHeight - 8
Rectangle {
id: wallpaperItem
anchors.fill: parent
anchors.margins: 4
color: Qt.darker(Data.ThemeManager.bgColor, 1.2)
radius: 20
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
}
// Wallpaper preview image with viewport-based loading
Image {
id: wallpaperImage
anchors.fill: parent
anchors.margins: 4
source: modelData
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false // Memory optimization - no image caching
sourceSize.width: Math.min(width, 150) // Reduced resolution for memory
sourceSize.height: Math.min(height, 90)
// Only load when visible in viewport - major memory optimization
visible: parent.parent.y >= wallpaperGrid.contentY - parent.parent.height &&
parent.parent.y <= wallpaperGrid.contentY + wallpaperGrid.height
// Layer effects disabled for memory savings
// layer.enabled: true
// layer.effect: OpacityMask {
// maskSource: Rectangle {
// width: wallpaperImage.width
// height: wallpaperImage.height
// radius: 18
// }
// }
}
// Current wallpaper selection indicator
Rectangle {
visible: modelData === Data.WallpaperManager.currentWallpaper
anchors.fill: parent
radius: parent.radius
color: "transparent"
border.color: Data.ThemeManager.accentColor
border.width: 2
}
// Hover and click handling
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: wallpaperItem.scale = 1.05
onExited: wallpaperItem.scale = 1.0
onClicked: {
Data.WallpaperManager.setWallpaper(modelData)
// Stays in wallpaper tab after selection
}
}
}
}
}
}
Component.onCompleted: {
// Use lazy loading to only load wallpapers when this component is actually used
Data.WallpaperManager.ensureWallpapersLoaded()
}
}

View file

@ -0,0 +1,75 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// Calendar button
Rectangle {
id: calendarButton
width: 40
height: 80
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
property bool containsMouse: calendarMouseArea.containsMouse
property bool calendarVisible: false
property var calendarPopup: null
property var shell: null // Shell reference from parent
signal entered()
signal exited()
// Hover state management
onContainsMouseChanged: {
if (containsMouse) {
entered()
} else {
exited()
}
}
MouseArea {
id: calendarMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
toggleCalendar()
}
}
// Calendar icon
Label {
anchors.centerIn: parent
text: "calendar_month"
font.pixelSize: 24
font.family: "Material Symbols Outlined"
color: calendarButton.containsMouse || calendarButton.calendarVisible ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
// Toggle calendar popup
function toggleCalendar() {
if (!calendarPopup) {
var component = Qt.createComponent("root:/Widgets/Calendar/CalendarPopup.qml")
if (component.status === Component.Ready) {
calendarPopup = component.createObject(calendarButton.parent, {
"targetX": calendarButton.x + calendarButton.width + 10,
"shell": calendarButton.shell
})
} else if (component.status === Component.Error) {
console.log("Error loading calendar:", component.errorString())
return
}
}
if (calendarPopup) {
calendarVisible = !calendarVisible
calendarPopup.setClickMode(calendarVisible)
}
}
function hideCalendar() {
if (calendarPopup) {
calendarVisible = false
calendarPopup.setClickMode(false)
}
}
}

View file

@ -0,0 +1,297 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import "root:/Data" as Data
// Night light widget with pure Qt overlay (no external dependencies)
Rectangle {
id: root
property var shell: null
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
property bool containsMouse: nightLightMouseArea.containsMouse
property bool isActive: Data.Settings.nightLightEnabled
property real warmth: Data.Settings.nightLightWarmth // 0=no filter, 1=very warm (0-1 scale)
property real strength: isActive ? warmth : 0
property bool autoSchedulerActive: false // Flag to prevent manual override during auto changes
signal entered()
signal exited()
// Night light overlay window
property var overlayWindow: null
// Hover state management for parent components
onContainsMouseChanged: {
if (containsMouse) {
entered()
} else {
exited()
}
}
// Background with warm tint when active
Rectangle {
anchors.fill: parent
radius: parent.radius
color: isActive ? Qt.rgba(1.0, 0.6, 0.2, 0.15) : "transparent"
Behavior on color {
ColorAnimation { duration: 300 }
}
}
MouseArea {
id: nightLightMouseArea
anchors.fill: parent
hoverEnabled: true
// Right-click to cycle through warmth levels
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: function(mouse) {
if (mouse.button === Qt.RightButton) {
cycleWarmth()
} else {
toggleNightLight()
}
}
}
// Night light icon with dynamic color
Text {
anchors.centerIn: parent
text: isActive ? "light_mode" : "dark_mode"
font.pixelSize: 24
font.family: "Material Symbols Outlined"
color: isActive ?
Qt.rgba(1.0, 0.8 - strength * 0.3, 0.4 - strength * 0.2, 1.0) : // Warm orange when active
(containsMouse ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor)
Behavior on color {
ColorAnimation { duration: 200 }
}
}
// Warmth indicator dots
Row {
anchors.bottom: parent.bottom
anchors.bottomMargin: 6
anchors.horizontalCenter: parent.horizontalCenter
spacing: 3
visible: isActive && containsMouse
Repeater {
model: 3
delegate: Rectangle {
width: 4
height: 4
radius: 2
color: index < Math.ceil(warmth * 3) ?
Qt.rgba(1.0, 0.7 - index * 0.2, 0.3, 0.8) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Behavior on color {
ColorAnimation { duration: 150 }
}
}
}
}
// Watch for settings changes
Connections {
target: Data.Settings
function onNightLightEnabledChanged() {
if (Data.Settings.nightLightEnabled) {
createOverlay()
} else {
removeOverlay()
}
// Set manual override flag if this wasn't an automatic change
if (!autoSchedulerActive) {
Data.Settings.nightLightManualOverride = true
Data.Settings.nightLightManuallyEnabled = Data.Settings.nightLightEnabled
console.log("Manual night light change detected - override enabled, manually set to:", Data.Settings.nightLightEnabled)
}
}
function onNightLightWarmthChanged() {
updateOverlay()
}
}
// Functions to control night light
function toggleNightLight() {
Data.Settings.nightLightEnabled = !Data.Settings.nightLightEnabled
}
function cycleWarmth() {
// Cycle through warmth levels: 0.2 -> 0.4 -> 0.6 -> 1.0 -> 0.2
var newWarmth = warmth >= 1.0 ? 0.2 : (warmth >= 0.6 ? 1.0 : warmth + 0.2)
Data.Settings.nightLightWarmth = newWarmth
}
function createOverlay() {
if (overlayWindow) return
var qmlString = `
import QtQuick
import Quickshell
import Quickshell.Wayland
PanelWindow {
id: nightLightOverlay
screen: Quickshell.primaryScreen || Quickshell.screens[0]
anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
color: "transparent"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.namespace: "quickshell-nightlight"
exclusiveZone: 0
// Click-through overlay
mask: Region {}
Rectangle {
id: overlayRect
anchors.fill: parent
color: "transparent" // Initial color, will be set by parent
// Smooth transitions when warmth changes
Behavior on color {
ColorAnimation { duration: 300 }
}
}
// Function to update overlay color
function updateColor(newWarmth) {
overlayRect.color = Qt.rgba(1.0, 0.8 - newWarmth * 0.4, 0.3 - newWarmth * 0.25, 0.1 + newWarmth * 0.2)
}
}
`
try {
overlayWindow = Qt.createQmlObject(qmlString, root)
// Set initial color
updateOverlay()
} catch (e) {
console.error("Failed to create night light overlay:", e)
}
}
function updateOverlay() {
if (overlayWindow && overlayWindow.updateColor) {
overlayWindow.updateColor(warmth)
}
}
function removeOverlay() {
if (overlayWindow) {
overlayWindow.destroy()
overlayWindow = null
}
}
// Preset warmth levels for easy access
function setLow() { Data.Settings.nightLightWarmth = 0.2 } // Light warmth
function setMedium() { Data.Settings.nightLightWarmth = 0.4 } // Medium warmth
function setHigh() { Data.Settings.nightLightWarmth = 0.6 } // High warmth
function setMax() { Data.Settings.nightLightWarmth = 1.0 } // Maximum warmth
// Auto-enable based on time (basic sunset/sunrise simulation)
Timer {
interval: 60000 // Check every minute
running: true
repeat: true
onTriggered: checkAutoEnable()
}
function checkAutoEnable() {
if (!Data.Settings.nightLightAuto) return
var now = new Date()
var hour = now.getHours()
var minute = now.getMinutes()
var startHour = Data.Settings.nightLightStartHour || 20
var endHour = Data.Settings.nightLightEndHour || 6
// Handle overnight schedules (e.g., 20:00 to 6:00)
var shouldBeActive = false
if (startHour > endHour) {
// Overnight: active from startHour onwards OR before endHour
shouldBeActive = (hour >= startHour || hour < endHour)
} else if (startHour < endHour) {
// Same day: active between startHour and endHour
shouldBeActive = (hour >= startHour && hour < endHour)
} else {
// startHour === endHour: never auto-enable
shouldBeActive = false
}
// Debug logging
console.log(`Night Light Auto Check: ${hour}:${minute.toString().padStart(2, '0')} - Should be active: ${shouldBeActive}, Currently active: ${Data.Settings.nightLightEnabled}, Manual override: ${Data.Settings.nightLightManualOverride}`)
// Smart override logic - only block conflicting actions
if (Data.Settings.nightLightManualOverride) {
// If user manually enabled, allow auto-disable but block auto-enable
if (Data.Settings.nightLightManuallyEnabled && !shouldBeActive && Data.Settings.nightLightEnabled) {
console.log("Auto-disabling night light (respecting schedule after manual enable)")
autoSchedulerActive = true
Data.Settings.nightLightEnabled = false
Data.Settings.nightLightManualOverride = false // Reset after respecting schedule
autoSchedulerActive = false
return
}
// If user manually disabled, block auto-enable until next cycle
else if (!Data.Settings.nightLightManuallyEnabled && shouldBeActive && !Data.Settings.nightLightEnabled) {
// Check if this is the start of a new schedule cycle
var isNewCycle = (hour === startHour && minute === 0)
if (isNewCycle) {
console.log("New schedule cycle starting - resetting manual override")
Data.Settings.nightLightManualOverride = false
} else {
console.log("Manual disable override active - skipping auto-enable")
return
}
}
// Other cases - reset override and continue
else {
Data.Settings.nightLightManualOverride = false
}
}
// Auto-enable when schedule starts
if (shouldBeActive && !Data.Settings.nightLightEnabled) {
console.log("Auto-enabling night light")
autoSchedulerActive = true
Data.Settings.nightLightEnabled = true
autoSchedulerActive = false
}
// Auto-disable when schedule ends
else if (!shouldBeActive && Data.Settings.nightLightEnabled) {
console.log("Auto-disabling night light")
autoSchedulerActive = true
Data.Settings.nightLightEnabled = false
autoSchedulerActive = false
}
}
// Cleanup on destruction
Component.onDestruction: {
removeOverlay()
}
// Initialize overlay state based on settings
Component.onCompleted: {
if (Data.Settings.nightLightEnabled) {
createOverlay()
}
}
}

View file

@ -0,0 +1,67 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import "root:/Data" as Data
// Screen recording toggle button
Rectangle {
id: root
required property var shell
required property bool isRecording
radius: 20
signal recordingRequested()
signal stopRecordingRequested()
signal mouseChanged(bool containsMouse)
// Dynamic color: accent when recording/hovered, gray otherwise
color: isRecording ? Data.ThemeManager.accentColor :
(mouseArea.containsMouse ? Data.ThemeManager.accentColor : Qt.darker(Data.ThemeManager.bgColor, 1.15))
property bool isHovered: mouseArea.containsMouse
readonly property alias containsMouse: mouseArea.containsMouse
// Button content with icon and text
RowLayout {
anchors.centerIn: parent
spacing: 10
// Recording state icon
Text {
text: isRecording ? "stop_circle" : "radio_button_unchecked"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: isRecording || mouseArea.containsMouse ? "#ffffff" : Data.ThemeManager.fgColor
Layout.alignment: Qt.AlignVCenter
}
// Recording state label
Label {
text: isRecording ? "Stop Recording" : "Start Recording"
font.family: "Roboto"
font.pixelSize: 13
font.weight: Font.Medium
color: isRecording || mouseArea.containsMouse ? "#ffffff" : Data.ThemeManager.fgColor
Layout.alignment: Qt.AlignVCenter
}
}
// Click handling and hover detection
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onContainsMouseChanged: root.mouseChanged(containsMouse)
onClicked: {
if (isRecording) {
root.stopRecordingRequested()
} else {
root.recordingRequested()
}
}
}
}

View file

@ -0,0 +1,45 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
// Simple theme toggle button with hover feedback
Rectangle {
id: root
property var shell: null
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
property bool containsMouse: themeMouseArea.containsMouse
property bool menuJustOpened: false
signal entered()
signal exited()
// Hover state management for parent components
onContainsMouseChanged: {
if (containsMouse) {
entered()
} else if (!menuJustOpened) {
exited()
}
}
MouseArea {
id: themeMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Data.ThemeManager.toggleTheme()
}
}
// Theme toggle icon with color feedback
Label {
anchors.centerIn: parent
text: "contrast"
font.pixelSize: 24
font.family: "Material Symbols Outlined"
color: containsMouse ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
}

View file

@ -0,0 +1,243 @@
import Quickshell.Io
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import "root:/Data/" as Data
// User profile card
Rectangle {
id: root
required property var shell
property url avatarSource: Data.Settings.avatarSource
property string userName: "" // will be set by process output
property string userInfo: "" // will hold uptime string
property bool isActive: false
property bool isHovered: false // track hover state
radius: 20
width: 220
height: 80
// Dynamic color based on hover and active states
color: {
if (isActive) {
return isHovered ?
Qt.lighter(Data.ThemeManager.accentColor, 1.1) :
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
} else {
return isHovered ?
Qt.lighter(Data.ThemeManager.accentColor, 1.2) :
Qt.lighter(Data.ThemeManager.bgColor, 1.15)
}
}
border.width: isActive ? 2 : 1
border.color: isActive ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.3)
Row {
anchors.fill: parent
anchors.margins: 14
spacing: 12
anchors.verticalCenter: parent.verticalCenter
// Avatar
Rectangle {
id: avatarCircle
width: 52
height: 52
radius: 20
clip: true
border.color: Data.ThemeManager.accentColor
border.width: 3
color: "transparent"
Image {
id: avatarImage
anchors.fill: parent
anchors.margins: 2
source: Data.Settings.avatarSource
fillMode: Image.PreserveAspectCrop
cache: false
visible: false // Hidden for masking
asynchronous: true
sourceSize.width: 48 // Memory optimization
sourceSize.height: 48
}
// Apply circular mask to avatar
OpacityMask {
anchors.fill: avatarImage
source: avatarImage
cached: true // Cache to reduce ShaderEffect issues
maskSource: Rectangle {
width: avatarImage.width
height: avatarImage.height
radius: 18 // Proportional to parent radius
visible: false
}
}
}
// User information text
Column {
spacing: 4
anchors.verticalCenter: parent.verticalCenter
width: parent.width - avatarCircle.width - gifContainer.width - parent.spacing * 2
Text {
width: parent.width
text: root.userName === "" ? "Loading..." : root.userName
font.family: "Roboto"
font.pixelSize: 16
font.bold: true
color: isHovered || root.isActive ? "#ffffff" : Data.ThemeManager.accentColor
elide: Text.ElideRight
maximumLineCount: 1
}
Text {
width: parent.width
text: root.userInfo === "" ? "Loading uptime..." : root.userInfo
font.family: "Roboto"
font.pixelSize: 11
font.bold: true
color: isHovered || root.isActive ? "#cccccc" : Qt.lighter(Data.ThemeManager.accentColor, 1.6)
elide: Text.ElideRight
maximumLineCount: 1
}
}
// Animated GIF with rounded corners
Rectangle {
id: gifContainer
width: 80
height: 80
radius: 12
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
AnimatedImage {
id: animatedImage
source: "root:/Assets/UserProfile.gif"
anchors.fill: parent
fillMode: Image.PreserveAspectFit
playing: true
cache: false
speed: 1.0
asynchronous: true
}
// Apply rounded corner mask to GIF
layer.enabled: true
layer.effect: OpacityMask {
cached: true // Cache to reduce ShaderEffect issues
maskSource: Rectangle {
width: gifContainer.width
height: gifContainer.height
radius: gifContainer.radius
visible: false
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: root.isHovered = true
onExited: root.isHovered = false
}
// Get current username
Process {
id: usernameProcess
running: true // Always run to get username
command: ["sh", "-c", "whoami"]
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
const line = data.trim();
if (line.length > 0) {
root.userName = line.charAt(0).toUpperCase() + line.slice(1);
}
}
}
}
// Get system uptime with parsing for readable format
Process {
id: uptimeProcess
running: false
command: ["sh", "-c", "uptime"] // Use basic uptime command
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
const line = data.trim();
if (line.length > 0) {
// Parse uptime output: " 10:30:00 up 1:23, 2 users, load average: 0.08, 0.02, 0.01"
const match = line.match(/up\s+(.+?),\s+\d+\s+user/);
if (match && match[1]) {
root.userInfo = "Up: " + match[1].trim();
} else {
// Fallback parsing for different uptime formats
const upIndex = line.indexOf("up ");
if (upIndex !== -1) {
const afterUp = line.substring(upIndex + 3);
const commaIndex = afterUp.indexOf(",");
if (commaIndex !== -1) {
root.userInfo = "Up: " + afterUp.substring(0, commaIndex).trim();
} else {
root.userInfo = "Up: " + afterUp.trim();
}
} else {
root.userInfo = "Uptime unknown";
}
}
} else {
root.userInfo = "Uptime unknown";
}
}
}
stderr: SplitParser {
splitMarker: "\n"
onRead: (data) => {
console.log("Uptime error:", data);
root.userInfo = "Uptime error";
}
}
}
// Update uptime every 5 minutes
Timer {
id: uptimeTimer
interval: 300000 // Update every 5 minutes
running: true // Always run the uptime timer
repeat: true
onTriggered: {
uptimeProcess.running = false
uptimeProcess.running = true
}
}
Component.onCompleted: {
uptimeProcess.running = true // Start uptime process on component load
}
Component.onDestruction: {
if (usernameProcess.running) {
usernameProcess.running = false
}
if (uptimeProcess.running) {
uptimeProcess.running = false
}
if (uptimeTimer.running) {
uptimeTimer.running = false
}
}
}

View file

@ -0,0 +1,354 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
// Weather display widget
Rectangle {
id: root
required property var shell
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
property bool containsMouse: weatherMouseArea.containsMouse || (forecastPopup.visible && forecastPopup.containsMouse)
property bool menuJustOpened: false
signal entered()
signal exited()
// Hover state management for parent components
onContainsMouseChanged: {
if (containsMouse) {
entered()
} else if (!menuJustOpened && !forecastPopup.visible) {
exited()
}
}
// Maps WMO weather condition codes and text descriptions to Material Design icons
function getWeatherIcon(condition) {
if (!condition) return "light_mode"
const c = condition.toString()
// WMO weather interpretation codes to Material Design icons
const iconMap = {
"0": "light_mode", // Clear sky
"1": "light_mode", // Mainly clear
"2": "cloud", // Partly cloudy
"3": "cloud", // Overcast
"45": "foggy", // Fog
"48": "foggy", // Depositing rime fog
"51": "water_drop", // Light drizzle
"53": "water_drop", // Moderate drizzle
"55": "water_drop", // Dense drizzle
"61": "water_drop", // Slight rain
"63": "water_drop", // Moderate rain
"65": "water_drop", // Heavy rain
"71": "ac_unit", // Slight snow
"73": "ac_unit", // Moderate snow
"75": "ac_unit", // Heavy snow
"80": "water_drop", // Slight rain showers
"81": "water_drop", // Moderate rain showers
"82": "water_drop", // Violent rain showers
"95": "thunderstorm", // Thunderstorm
"96": "thunderstorm", // Thunderstorm with light hail
"99": "thunderstorm" // Thunderstorm with heavy hail
}
if (iconMap[c]) return iconMap[c]
// Fallback text matching for non-WMO weather APIs
const textMap = {
"clear sky": "light_mode",
"mainly clear": "light_mode",
"partly cloudy": "cloud",
"overcast": "cloud",
"fog": "foggy",
"drizzle": "water_drop",
"rain": "water_drop",
"snow": "ac_unit",
"thunderstorm": "thunderstorm"
}
const lower = condition.toLowerCase()
for (let key in textMap) {
if (lower.includes(key)) return textMap[key]
}
return "help" // Unknown condition fallback
}
// Hover trigger for forecast popup
MouseArea {
id: weatherMouseArea
anchors.fill: parent
hoverEnabled: true
onEntered: {
menuJustOpened = true
forecastPopup.open()
Qt.callLater(() => menuJustOpened = false)
}
onExited: {
if (!forecastPopup.containsMouse && !menuJustOpened) {
forecastPopup.close()
}
}
}
// Compact weather display (icon and temperature)
RowLayout {
id: weatherLayout
anchors.centerIn: parent
spacing: 8
ColumnLayout {
spacing: 2
Layout.alignment: Qt.AlignVCenter
// Weather condition icon
Label {
text: {
if (shell.weatherLoading) return "refresh"
if (!shell.weatherData) return "help"
return root.getWeatherIcon(shell.weatherData.currentCondition)
}
font.pixelSize: 28
font.family: "Material Symbols Outlined"
color: Data.ThemeManager.accentColor
Layout.alignment: Qt.AlignHCenter
}
// Current temperature
Label {
text: {
if (shell.weatherLoading) return "Loading..."
if (!shell.weatherData) return "No weather data"
return shell.weatherData.currentTemp
}
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 20
font.bold: true
Layout.alignment: Qt.AlignHCenter
}
}
}
// Forecast popup
Popup {
id: forecastPopup
y: parent.height + 28
x: Math.min(0, parent.width - width)
width: 300
height: 226
padding: 12
background: Rectangle {
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
border.width: 1
border.color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
property bool containsMouse: forecastMouseArea.containsMouse
onVisibleChanged: {
if (visible) {
entered()
} else if (!weatherMouseArea.containsMouse && !menuJustOpened) {
exited()
}
}
// Hover area for popup persistence
MouseArea {
id: forecastMouseArea
anchors.fill: parent
hoverEnabled: true
onExited: {
if (!weatherMouseArea.containsMouse && !menuJustOpened) {
forecastPopup.close()
}
}
}
ColumnLayout {
id: forecastColumn
anchors.fill: parent
anchors.margins: 10
spacing: 8
// Current weather detailed view
RowLayout {
Layout.fillWidth: true
spacing: 12
// Large weather icon
Label {
text: shell.weatherData ? root.getWeatherIcon(shell.weatherData.currentCondition) : ""
font.pixelSize: 48
font.family: "Material Symbols Outlined"
color: Data.ThemeManager.accentColor
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
// Weather condition description
Label {
text: shell.weatherData ? shell.weatherData.currentCondition : ""
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 14
font.bold: true
Layout.fillWidth: true
elide: Text.ElideRight
}
// Weather metrics: temperature, wind, direction
RowLayout {
spacing: 8
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
// Temperature metric
RowLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Label {
text: "thermostat"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: Data.ThemeManager.accentColor
}
Label {
text: shell.weatherData ? shell.weatherData.currentTemp : ""
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
}
}
Rectangle {
width: 1
height: 12
color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
// Wind speed metric
RowLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Label {
text: "air"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: Data.ThemeManager.accentColor
}
Label {
text: {
if (!shell.weatherData || !shell.weatherData.details) return ""
const windInfo = shell.weatherData.details.find(d => d.startsWith("Wind:"))
return windInfo ? windInfo.split(": ")[1] : ""
}
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
}
}
Rectangle {
width: 1
height: 12
color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
// Wind direction metric
RowLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Label {
text: "explore"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: Data.ThemeManager.accentColor
}
Label {
text: {
if (!shell.weatherData || !shell.weatherData.details) return ""
const dirInfo = shell.weatherData.details.find(d => d.startsWith("Direction:"))
return dirInfo ? dirInfo.split(": ")[1] : ""
}
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
}
}
Item {
Layout.fillWidth: true
}
}
}
}
// Section separator
Rectangle {
height: 1
Layout.fillWidth: true
color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
Label {
text: "3-Day Forecast"
color: Data.ThemeManager.accentColor
font.family: "Roboto"
font.pixelSize: 12
font.bold: true
}
// Three-column forecast cards
Row {
spacing: 8
Layout.fillWidth: true
Repeater {
model: shell.weatherData ? shell.weatherData.forecast : []
delegate: Column {
width: (parent.width - 16) / 3
spacing: 2
// Day name
Label {
text: modelData.dayName
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 10
font.bold: true
anchors.horizontalCenter: parent.horizontalCenter
}
// Weather icon
Label {
text: root.getWeatherIcon(modelData.condition)
font.pixelSize: 16
font.family: "Material Symbols Outlined"
color: Data.ThemeManager.accentColor
anchors.horizontalCenter: parent.horizontalCenter
}
// Temperature range
Label {
text: modelData.minTemp + "° - " + modelData.maxTemp + "°"
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 10
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
}
}

View file

@ -0,0 +1,147 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// Calendar tab content
Item {
id: calendarTab
required property var shell
property bool isActive: false
Column {
anchors.fill: parent
spacing: 12
Text {
text: "Calendar"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "Roboto"
}
Rectangle {
width: parent.width
height: parent.height - parent.children[0].height - parent.spacing
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
radius: 20
clip: true
Loader {
anchors.fill: parent
anchors.margins: 16
active: calendarTab.isActive
sourceComponent: active ? calendarComponent : null
}
}
}
Component {
id: calendarComponent
Item {
id: calendarRoot
property var shell: calendarTab.shell
readonly property date currentDate: new Date()
property int month: currentDate.getMonth()
property int year: currentDate.getFullYear()
readonly property int currentDay: currentDate.getDate()
Column {
anchors.fill: parent
anchors.margins: 8
spacing: 8
// Month/Year header
Text {
text: Qt.locale("en_US").monthName(calendarRoot.month) + " " + calendarRoot.year
color: Data.ThemeManager.accentColor
font.bold: true
width: parent.width
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 16
height: 24
}
// Weekday headers (Monday-Sunday)
Grid {
columns: 7
rowSpacing: 2
columnSpacing: 0
width: parent.width
height: 18
Repeater {
model: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
delegate: Text {
text: modelData
color: Data.ThemeManager.fgColor
font.bold: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width / 7
height: 18
font.pixelSize: 11
}
}
}
// Calendar grid - single unified grid
Grid {
columns: 7
rowSpacing: 3
columnSpacing: 3
width: parent.width
property int firstDayOfMonth: new Date(calendarRoot.year, calendarRoot.month, 1).getDay()
property int daysInMonth: new Date(calendarRoot.year, calendarRoot.month + 1, 0).getDate()
property int startOffset: (firstDayOfMonth === 0) ? 6 : firstDayOfMonth - 1 // Convert Sunday=0 to Monday=0
property int prevMonthDays: new Date(calendarRoot.year, calendarRoot.month, 0).getDate()
// Single repeater for all 42 calendar cells (6 weeks × 7 days)
Repeater {
model: 42
delegate: Rectangle {
width: (parent.width - (parent.columnSpacing * 6)) / 7
height: 26
radius: 13
// Calculate which day this cell represents
readonly property int dayNumber: {
if (index < parent.startOffset) {
// Previous month
return parent.prevMonthDays - parent.startOffset + index + 1
} else if (index < parent.startOffset + parent.daysInMonth) {
// Current month
return index - parent.startOffset + 1
} else {
// Next month
return index - parent.startOffset - parent.daysInMonth + 1
}
}
readonly property bool isCurrentMonth: index >= parent.startOffset && index < (parent.startOffset + parent.daysInMonth)
readonly property bool isToday: isCurrentMonth && dayNumber === calendarRoot.currentDay &&
calendarRoot.month === calendarRoot.currentDate.getMonth() &&
calendarRoot.year === calendarRoot.currentDate.getFullYear()
color: isToday ? Data.ThemeManager.accentColor :
isCurrentMonth ? Data.ThemeManager.bgColor : Qt.darker(Data.ThemeManager.bgColor, 1.4)
Text {
text: dayNumber
anchors.centerIn: parent
color: isToday ? Data.ThemeManager.bgColor :
isCurrentMonth ? Data.ThemeManager.fgColor : Qt.darker(Data.ThemeManager.fgColor, 1.5)
font.bold: isToday
font.pixelSize: 12
font.family: "Roboto"
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,110 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
import "root:/Widgets/System" as System
// Clipboard tab content
Item {
id: clipboardTab
required property var shell
property bool isActive: false
Column {
anchors.fill: parent
spacing: 16
RowLayout {
width: parent.width
spacing: 16
Text {
text: "Clipboard History"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "Roboto"
}
Item { Layout.fillWidth: true }
Rectangle {
width: clearClipText.implicitWidth + 16
height: 24
radius: 12
color: clearClipMouseArea.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : "transparent"
border.color: Data.ThemeManager.accentColor
border.width: 1
Text {
id: clearClipText
anchors.centerIn: parent
text: "Clear All"
color: Data.ThemeManager.accentColor
font.family: "Roboto"
font.pixelSize: 11
}
MouseArea {
id: clearClipMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (clipboardLoader.item && clipboardLoader.item.children[0]) {
let clipComponent = clipboardLoader.item.children[0]
if (clipComponent.clearClipboardHistory) {
clipComponent.clearClipboardHistory()
}
}
}
}
}
}
Rectangle {
width: parent.width
height: parent.height - parent.children[0].height - parent.spacing
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
radius: 20
clip: true
Loader {
id: clipboardLoader
anchors.fill: parent
anchors.margins: 20
active: clipboardTab.isActive
sourceComponent: active ? clipboardHistoryComponent : null
onLoaded: {
if (item && item.children[0]) {
item.children[0].refreshClipboardHistory()
}
}
}
}
}
Component {
id: clipboardHistoryComponent
Item {
anchors.fill: parent
System.Cliphist {
id: cliphistComponent
anchors.fill: parent
shell: clipboardTab.shell
Component.onCompleted: {
for (let i = 0; i < children.length; i++) {
let child = children[i]
if (child.objectName === "contentColumn" || child.toString().includes("ColumnLayout")) {
if (child.children && child.children.length > 0) {
child.children[0].visible = false
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,155 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
import "root:/Widgets/System" as System
import "../components/widgets" as Widgets
import "../components/controls" as Controls
import "../components/system" as SystemComponents
// Main dashboard content (tab 0)
Item {
id: mainDashboard
// Properties from parent
required property var shell
required property bool isRecording
required property var triggerMouseArea
// Signals to forward
signal recordingRequested()
signal stopRecordingRequested()
signal systemActionRequested(string action)
signal performanceActionRequested(string action)
// Hover detection for auto-hide
property bool isHovered: {
const mouseStates = {
userProfileHovered: userProfile ? userProfile.isHovered : false,
weatherDisplayHovered: weatherDisplay ? weatherDisplay.containsMouse : false,
recordingButtonHovered: recordingButton ? recordingButton.isHovered : false,
controlsHovered: controls ? controls.containsMouse : false,
trayHovered: trayMouseArea ? trayMouseArea.containsMouse : false,
systemTrayHovered: systemTrayModule ? systemTrayModule.containsMouse : false,
trayMenuHovered: inlineTrayMenu ? inlineTrayMenu.containsMouse : false,
trayMenuVisible: inlineTrayMenu ? inlineTrayMenu.visible : false
}
return Object.values(mouseStates).some(state => state)
}
// Night Light overlay controller (invisible - manages screen overlay)
Widgets.NightLight {
id: nightLightController
shell: mainDashboard.shell
visible: false // This widget manages overlay windows, doesn't need to be visible
}
Column {
anchors.fill: parent
spacing: 28
// User profile row with weather
Row {
width: parent.width
spacing: 18
Widgets.UserProfile {
id: userProfile
width: parent.width - weatherDisplay.width - parent.spacing
height: 80
shell: mainDashboard.shell
}
Widgets.WeatherDisplay {
id: weatherDisplay
width: parent.width * 0.18
height: userProfile.height
shell: mainDashboard.shell
}
}
// Recording and system controls section
Column {
width: parent.width
spacing: 28
Widgets.RecordingButton {
id: recordingButton
width: parent.width
height: 48
shell: mainDashboard.shell
isRecording: mainDashboard.isRecording
onRecordingRequested: mainDashboard.recordingRequested()
onStopRecordingRequested: mainDashboard.stopRecordingRequested()
}
Controls.Controls {
id: controls
width: parent.width
isRecording: mainDashboard.isRecording
shell: mainDashboard.shell
onPerformanceActionRequested: function(action) { mainDashboard.performanceActionRequested(action) }
onSystemActionRequested: function(action) { mainDashboard.systemActionRequested(action) }
}
}
// System tray integration with menu
Column {
id: systemTraySection
width: parent.width
spacing: 8
property bool containsMouse: trayMouseArea.containsMouse || systemTrayModule.containsMouse
Rectangle {
id: trayBackground
width: parent.width
height: 40
radius: 20
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
property bool isActive: false
MouseArea {
id: trayMouseArea
anchors.fill: parent
anchors.margins: -10
hoverEnabled: true
propagateComposedEvents: true
preventStealing: false
onEntered: trayBackground.isActive = true
onExited: {
// Only deactivate if we're not hovering over tray menu or system tray module
if (!inlineTrayMenu.visible && !inlineTrayMenu.containsMouse) {
Qt.callLater(function() {
if (!systemTrayModule.containsMouse && !inlineTrayMenu.containsMouse && !inlineTrayMenu.visible) {
trayBackground.isActive = false
}
})
}
}
}
System.SystemTray {
id: systemTrayModule
anchors.centerIn: parent
shell: mainDashboard.shell
bar: parent
trayMenu: inlineTrayMenu
}
}
}
SystemComponents.TrayMenu {
id: inlineTrayMenu
parent: mainDashboard
width: parent.width
menu: null
systemTrayY: systemTraySection.y
systemTrayHeight: systemTraySection.height
z: 100 // High z-index to appear above other content
onHideRequested: trayBackground.isActive = false
}
}
}

View file

@ -0,0 +1,46 @@
import QtQuick
import "root:/Data" as Data
import "../components/media" as Media
// Music tab content
Item {
id: musicTab
required property var shell
property bool isActive: false
Column {
anchors.fill: parent
spacing: 16
Text {
text: "Music Player"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "Roboto"
}
Rectangle {
width: parent.width
height: parent.height - parent.children[0].height - parent.spacing
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
radius: 20
clip: true
Loader {
anchors.fill: parent
anchors.margins: 20
active: musicTab.isActive
sourceComponent: active ? musicPlayerComponent : null
}
}
}
Component {
id: musicPlayerComponent
Media.MusicPlayer {
shell: musicTab.shell
}
}
}

View file

@ -0,0 +1,106 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
import "root:/Widgets/Notifications" as Notifications
// Notification tab content
Item {
id: notificationTab
required property var shell
property bool isActive: false
Column {
anchors.fill: parent
spacing: 16
RowLayout {
width: parent.width
spacing: 16
Text {
text: "Notification History"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "Roboto"
}
Text {
text: "(" + (notificationTab.shell.notificationHistory ? notificationTab.shell.notificationHistory.count : 0) + ")"
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
opacity: 0.7
Layout.alignment: Qt.AlignVCenter
}
Item { Layout.fillWidth: true }
Rectangle {
width: clearNotifText.implicitWidth + 16
height: 24
radius: 12
color: clearNotifMouseArea.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : "transparent"
border.color: Data.ThemeManager.accentColor
border.width: 1
Text {
id: clearNotifText
anchors.centerIn: parent
text: "Clear All"
color: Data.ThemeManager.accentColor
font.family: "Roboto"
font.pixelSize: 11
}
MouseArea {
id: clearNotifMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: notificationTab.shell.notificationHistory.clear()
}
}
}
Rectangle {
width: parent.width
height: parent.height - parent.children[0].height - parent.spacing
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
radius: 20
clip: true
Loader {
anchors.fill: parent
anchors.margins: 20
active: notificationTab.isActive
sourceComponent: active ? notificationHistoryComponent : null
}
}
}
Component {
id: notificationHistoryComponent
Item {
anchors.fill: parent
Notifications.NotificationHistory {
anchors.fill: parent
shell: notificationTab.shell
clip: true
Component.onCompleted: {
for (let i = 0; i < children.length; i++) {
let child = children[i]
if (child.objectName === "contentColumn" || child.toString().includes("ColumnLayout")) {
if (child.children && child.children.length > 0) {
child.children[0].visible = false
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,153 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
import "../components/settings" as SettingsComponents
// Settings tab content with modular, collapsible categories
Item {
id: settingsTab
required property var shell
property bool isActive: false
// Track when any text input has focus for keyboard management
property bool anyTextInputFocused: {
try {
return (notificationSettings && notificationSettings.anyTextInputFocused) ||
(systemSettings && systemSettings.anyTextInputFocused) ||
(weatherSettings && weatherSettings.anyTextInputFocused)
} catch (e) {
return false
}
}
// Header
Text {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 20
text: "Settings"
color: Data.ThemeManager.accentColor
font.pixelSize: 24
font.bold: true
font.family: "Roboto"
}
// Scrollable content
ScrollView {
anchors.top: header.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.topMargin: 16
anchors.leftMargin: 20
anchors.rightMargin: 20
anchors.bottomMargin: 20
clip: true
contentWidth: width - 5 // Reserve space for scrollbar
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Column {
width: parent.width - 15 // Match contentWidth
spacing: 16
// VISUAL SETTINGS
// Appearance Category
SettingsComponents.SettingsCategory {
id: appearanceCategory
width: parent.width
title: "Appearance"
icon: "palette"
content: Component {
SettingsComponents.AppearanceSettings {
width: parent.width
}
}
}
// CORE SYSTEM SETTINGS
// System Category
SettingsComponents.SettingsCategory {
id: systemCategory
width: parent.width
title: "System"
icon: "settings"
content: Component {
SettingsComponents.SystemSettings {
id: systemSettings
width: parent.width
}
}
}
// Notifications Category
SettingsComponents.SettingsCategory {
id: notificationsCategory
width: parent.width
title: "Notifications"
icon: "notifications"
content: Component {
SettingsComponents.NotificationSettings {
id: notificationSettings
width: parent.width
}
}
}
// 🎵 MEDIA & EXTERNAL SERVICES
// Music Player Category
SettingsComponents.SettingsCategory {
id: musicPlayerCategory
width: parent.width
title: "Music Player"
icon: "music_note"
content: Component {
SettingsComponents.MusicPlayerSettings {
width: parent.width
}
}
}
// Weather Category
SettingsComponents.SettingsCategory {
id: weatherCategory
width: parent.width
title: "Weather"
icon: "wb_sunny"
content: Component {
SettingsComponents.WeatherSettings {
id: weatherSettings
width: parent.width
shell: settingsTab.shell
}
}
}
// ACCESSIBILITY & COMFORT
// Night Light Category
SettingsComponents.SettingsCategory {
id: nightLightCategory
width: parent.width
title: "Night Light"
icon: "dark_mode"
content: Component {
SettingsComponents.NightLightSettings {
width: parent.width
}
}
}
}
}
}

View file

@ -0,0 +1,45 @@
import QtQuick
import "root:/Data" as Data
import "../components/system" as SystemComponents
// Wallpaper tab content
Item {
id: wallpaperTab
property bool isActive: false
Column {
anchors.fill: parent
spacing: 16
Text {
text: "Wallpapers"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "Roboto"
}
Rectangle {
width: parent.width
height: parent.height - parent.children[0].height - parent.spacing
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
radius: 20
clip: true
Loader {
anchors.fill: parent
anchors.margins: 20
active: wallpaperTab.isActive
sourceComponent: active ? wallpaperSelectorComponent : null
}
}
}
Component {
id: wallpaperSelectorComponent
SystemComponents.WallpaperSelector {
isVisible: parent && parent.parent && parent.parent.visible
}
}
}

View file

@ -0,0 +1,262 @@
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import "root:/Data" as Data
// Authentication area
Column {
id: authColumn
anchors.centerIn: parent
anchors.verticalCenterOffset: 80
spacing: 20
width: 300
required property bool isVisible
required property string errorMessage
required property bool isAuthenticating
required property bool authSuccess
required property string usernameText
signal passwordEntered(string password)
// Expose password field
readonly property alias passwordField: passwordField
// Subtle slide up animation (after main slide)
transform: Translate {
id: authTransform
y: isVisible ? 0 : 50
Behavior on y {
SequentialAnimation {
PauseAnimation { duration: 600 } // Wait for main slide and time
NumberAnimation {
duration: 600
easing.type: Easing.OutBack
}
}
}
}
opacity: isVisible ? 1.0 : 0.0
Behavior on opacity {
SequentialAnimation {
PauseAnimation { duration: 700 } // Wait for time to appear
NumberAnimation {
duration: 600
easing.type: Easing.OutCubic
}
}
}
// User avatar with circular masking
Rectangle {
id: avatarContainer
width: 100
height: 100
radius: 50
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
border.color: Data.ThemeManager.accentColor
border.width: 4
clip: true
// Scale animation for avatar
scale: isVisible ? 1.0 : 0.0
Behavior on scale {
SequentialAnimation {
PauseAnimation { duration: 1000 } // Wait for auth area to appear
NumberAnimation {
duration: 400
easing.type: Easing.OutBack
}
}
}
Image {
id: avatarImage
anchors.fill: parent
anchors.margins: 4
source: Data.Settings.avatarSource
fillMode: Image.PreserveAspectCrop
cache: false
visible: false // Hidden for masking
asynchronous: true
sourceSize.width: 92
sourceSize.height: 92
}
// Apply circular mask to avatar
OpacityMask {
anchors.fill: avatarImage
source: avatarImage
maskSource: Rectangle {
width: avatarImage.width
height: avatarImage.height
radius: 46
visible: false
}
}
// Fallback icon if avatar fails to load
Text {
anchors.centerIn: parent
text: "person"
font.family: "Material Symbols Outlined"
font.pixelSize: 48
color: Data.ThemeManager.accentColor
visible: avatarImage.status !== Image.Ready
}
}
// Username display
Text {
id: usernameDisplay
anchors.horizontalCenter: parent.horizontalCenter
font.family: "FiraCode Nerd Font"
font.pixelSize: 18
color: Data.ThemeManager.primaryText
text: usernameText
}
// Password input field
Rectangle {
width: parent.width
height: 50
radius: 25
color: Data.ThemeManager.withOpacity(Data.ThemeManager.bgLighter, 0.4)
border.color: passwordField.activeFocus ? Data.ThemeManager.accentColor : Data.ThemeManager.withOpacity(Data.ThemeManager.border, 0.6)
border.width: 2
TextInput {
id: passwordField
anchors.fill: parent
anchors.margins: 15
echoMode: TextInput.Normal
font.family: "FiraCode Nerd Font"
font.pixelSize: 16
color: "transparent" // Hide the actual text
selectionColor: Data.ThemeManager.accentColor
selectByMouse: true
focus: isVisible
onAccepted: {
if (text.length > 0) {
passwordEntered(text)
}
}
// Password mask with better spaced dots
Row {
id: passwordDotsRow
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: 8
visible: passwordField.text.length > 0
property int previousLength: 0
Repeater {
id: passwordRepeater
model: passwordField.text.length
delegate: Rectangle {
id: passwordDot
width: 8
height: 8
radius: 4
color: Data.ThemeManager.primaryText
property bool isNewDot: index >= passwordDotsRow.previousLength
// Only animate new dots, existing ones stay visible
scale: isNewDot ? 0 : 1.0
opacity: isNewDot ? 0 : 1.0
ParallelAnimation {
running: passwordDot.isNewDot
NumberAnimation {
target: passwordDot
property: "scale"
from: 0
to: 1.0
duration: 200
easing.type: Easing.OutCubic
}
NumberAnimation {
target: passwordDot
property: "opacity"
from: 0
to: 1.0
duration: 150
easing.type: Easing.OutQuad
}
}
}
}
// Track length changes to identify new dots
Connections {
target: passwordField
function onTextChanged() {
// Update previous length after a short delay to allow new dots to be marked as new
Qt.callLater(function() {
passwordDotsRow.previousLength = passwordField.text.length
})
}
}
}
// Placeholder text
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
text: "Password"
font.family: passwordField.font.family
font.pixelSize: passwordField.font.pixelSize
color: Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.7)
visible: passwordField.text.length === 0 && !passwordField.activeFocus
}
}
}
// Error message
Text {
id: errorText
anchors.horizontalCenter: parent.horizontalCenter
font.family: "FiraCode Nerd Font"
font.pixelSize: 14
color: Data.ThemeManager.errorColor
text: errorMessage
visible: errorMessage !== ""
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
// Authentication status
Text {
id: statusText
anchors.horizontalCenter: parent.horizontalCenter
font.family: "FiraCode Nerd Font"
font.pixelSize: 14
color: authSuccess ? Data.ThemeManager.success : Data.ThemeManager.accentColorBright
text: {
if (authSuccess) return "Authentication successful!"
if (isAuthenticating) return "Authenticating..."
return ""
}
visible: isAuthenticating || authSuccess
}
// Public function to clear password
function clearPassword() {
passwordField.text = ""
passwordField.focus = true
}
// Public function to focus password field
function focusPassword() {
passwordField.focus = true
}
}

View file

@ -0,0 +1,299 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
import "root:/Data" as Data
import "root:/Core" as Core
// Custom lockscreen
PanelWindow {
id: lockScreen
required property var shell
property bool isLocked: false
property bool isAuthenticated: false
property string errorMessage: ""
property int failedAttempts: 0
property bool isAuthenticating: false
property bool authSuccess: false
property string usernameText: "Enter Password"
// Animation state - controlled by timer for proper timing
property bool animateIn: false
// Full screen coverage
screen: Quickshell.primaryScreen || Quickshell.screens[0]
anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
color: "transparent"
// Top layer to block everything
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
WlrLayershell.namespace: "quickshell-lockscreen"
visible: isLocked
// Timer for slide-in animation - more reliable than Qt.callLater
Timer {
id: slideInTimer
interval: 100 // Short delay to ensure window is fully rendered
running: false
onTriggered: {
console.log("slideInTimer triggered, setting animateIn = true")
animateIn = true
}
}
// Timer for slide-out animation before hiding window
Timer {
id: slideOutTimer
interval: 1000 // Wait for slide animation to complete
running: false
onTriggered: {
isLocked = false
authArea.clearPassword()
errorMessage = ""
failedAttempts = 0
authSuccess = false
}
}
// Timer to show success message before unlocking
Timer {
id: successTimer
interval: 1200 // Show success message for 1.2 seconds
running: false
onTriggered: {
unlock()
}
}
// Reset animation state when window becomes invisible
onVisibleChanged: {
if (!visible) {
animateIn = false
authSuccess = false
slideInTimer.stop()
slideOutTimer.stop()
successTimer.stop()
}
}
// Background component
LockscreenBackground {
id: background
isVisible: lockScreen.visible
}
// Main lockscreen content with slide-from-top animation
Item {
id: mainContent
anchors.fill: parent
focus: true // Enable focus for keyboard handling
// Dramatic slide animation - starts off-screen, slides down when animateIn is true
transform: Translate {
id: mainTransform
y: lockScreen.animateIn ? 0 : -lockScreen.height
Behavior on y {
NumberAnimation {
duration: 800
easing.type: Easing.OutCubic
}
}
}
// Scale animation for extra drama
scale: lockScreen.animateIn ? 1.0 : 0.98
Behavior on scale {
NumberAnimation {
duration: 800
easing.type: Easing.OutCubic
}
}
// Keyboard shortcuts
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
authArea.clearPassword()
errorMessage = ""
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (authArea.passwordField.text.length > 0) {
authenticate(authArea.passwordField.text)
}
}
}
// Authentication area component
AuthenticationArea {
id: authArea
isVisible: lockScreen.animateIn
errorMessage: lockScreen.errorMessage
isAuthenticating: lockScreen.isAuthenticating
authSuccess: lockScreen.authSuccess
usernameText: lockScreen.usernameText
onPasswordEntered: function(password) {
authenticate(password)
}
}
// Power buttons component
PowerButtons {
id: powerButtons
isVisible: lockScreen.animateIn
onRebootRequested: rebootProcess.running = true
onShutdownRequested: shutdownProcess.running = true
}
}
// Authentication process using proper PAM authentication
Process {
id: authProcess
property string password: ""
command: ["sh", "-c", "echo '" + password.replace(/'/g, "'\"'\"'") + "' | sudo -S -k true"]
running: false
onExited: function(exitCode) {
isAuthenticating = false
if (exitCode === 0) {
// Authentication successful
isAuthenticated = true
errorMessage = ""
authSuccess = true
// Show success message for a brief moment before unlocking
successTimer.start()
} else {
// Authentication failed
failedAttempts++
errorMessage = failedAttempts === 1 ? "Incorrect password" : `Incorrect password (${failedAttempts} attempts)`
authArea.clearPassword()
// Add delay for failed attempts
if (failedAttempts >= 3) {
lockoutTimer.start()
}
}
}
}
// Lockout timer for failed attempts
Timer {
id: lockoutTimer
interval: 30000 // 30 second lockout
onTriggered: {
errorMessage = ""
authArea.passwordField.enabled = true
authArea.focusPassword()
}
}
// Reboot process
Process {
id: rebootProcess
command: ["systemctl", "reboot"]
running: false
onExited: function(exitCode) { console.log("Reboot process completed with exit code:", exitCode) }
}
// Shutdown process
Process {
id: shutdownProcess
command: ["systemctl", "poweroff"]
running: false
onExited: function(exitCode) { console.log("Shutdown process completed with exit code:", exitCode) }
}
// Get current username
Process {
id: usernameProcess
command: ["whoami"]
running: lockScreen.isLocked
stdout: SplitParser {
onRead: function(data) {
const username = data.trim()
if (username) {
usernameText = username.charAt(0).toUpperCase() + username.slice(1)
}
}
}
}
// Public functions
function lock() {
console.log("Lockscreen.lock() called")
// Reset animation state FIRST, before making window visible
animateIn = false
isLocked = true
isAuthenticated = false
authSuccess = false
errorMessage = ""
failedAttempts = 0
authArea.clearPassword()
usernameProcess.running = true
// Trigger slide animation after a short delay
console.log("Starting slideInTimer")
slideInTimer.start()
}
function unlock() {
console.log("Lockscreen.unlock() called")
// Start slide-out animation first
animateIn = false
// Use timer for reliable timing before completely hiding
slideOutTimer.start()
}
function authenticate(password) {
if (isAuthenticating || password.length === 0) return
console.log("Authenticating...")
isAuthenticating = true
authSuccess = false
errorMessage = ""
// Use sudo authentication
authProcess.password = password
authProcess.running = true
}
// Focus management when locked state changes
onIsLockedChanged: {
console.log("isLocked changed to:", isLocked)
if (isLocked) {
mainContent.focus = true
authArea.focusPassword()
}
}
// Clean up processes on destruction
Component.onDestruction: {
if (authProcess.running) authProcess.running = false
if (rebootProcess.running) rebootProcess.running = false
if (shutdownProcess.running) shutdownProcess.running = false
if (usernameProcess.running) usernameProcess.running = false
if (lockoutTimer.running) lockoutTimer.running = false
if (slideInTimer.running) slideInTimer.running = false
if (slideOutTimer.running) slideOutTimer.running = false
if (successTimer.running) successTimer.running = false
}
}

View file

@ -0,0 +1,59 @@
import QtQuick
import Qt5Compat.GraphicalEffects
import "root:/Data" as Data
// Background with wallpaper
Rectangle {
id: backgroundContainer
anchors.fill: parent
color: Data.ThemeManager.bgColor
required property bool isVisible
// Fade-in animation for the whole background
opacity: isVisible ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: 500
easing.type: Easing.OutCubic
}
}
// Wallpaper background
Image {
id: wallpaperImage
anchors.fill: parent
source: Data.WallpaperManager.currentWallpaper ? "file://" + Data.WallpaperManager.currentWallpaper : ""
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false
}
// Dark overlay
Rectangle {
anchors.fill: parent
color: Data.ThemeManager.withOpacity(Data.ThemeManager.bgColor, 0.8)
}
// Blur effect overlay
GaussianBlur {
anchors.fill: wallpaperImage
source: wallpaperImage
radius: 32
samples: 65
// Blur animation - starts less blurred and increases
Behavior on radius {
NumberAnimation {
duration: 1200
easing.type: Easing.OutCubic
}
}
Component.onCompleted: {
if (isVisible) {
radius = 32
}
}
}
}

View file

@ -0,0 +1,87 @@
import QtQuick
import "root:/Data" as Data
// Reboot and shutdown buttons positioned at bottom right
Row {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 40
spacing: 16
required property bool isVisible
signal rebootRequested()
signal shutdownRequested()
// Fade in with delay
opacity: isVisible ? 1.0 : 0.0
Behavior on opacity {
SequentialAnimation {
PauseAnimation { duration: 900 } // Wait for most elements to appear
NumberAnimation {
duration: 500
easing.type: Easing.OutCubic
}
}
}
// Reboot button
Rectangle {
width: 45
height: 45
radius: 22
color: rebootMouseArea.containsMouse ? Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.3) : Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.2)
border.color: Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.6)
border.width: 1
Text {
anchors.centerIn: parent
text: "restart_alt"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: Data.ThemeManager.secondaryText
}
MouseArea {
id: rebootMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: rebootRequested()
}
Behavior on color {
ColorAnimation { duration: 200 }
}
}
// Shutdown button
Rectangle {
width: 45
height: 45
radius: 22
color: shutdownMouseArea.containsMouse ? Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.3) : Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.2)
border.color: Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.6)
border.width: 1
Text {
anchors.centerIn: parent
text: "power_settings_new"
font.family: "Material Symbols Outlined"
font.pixelSize: 18
color: Data.ThemeManager.secondaryText
}
MouseArea {
id: shutdownMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: shutdownRequested()
}
Behavior on color {
ColorAnimation { duration: 200 }
}
}
}

View file

@ -0,0 +1,72 @@
import QtQuick
import "root:/Data" as Data
// Time and date display
Column {
id: timeColumn
anchors.top: parent.top
anchors.left: parent.left
anchors.topMargin: 60
anchors.leftMargin: 60
spacing: 16
required property bool isVisible
// Subtle slide-left animation (after main slide)
transform: Translate {
id: timeTransform
x: isVisible ? 0 : -100
Behavior on x {
SequentialAnimation {
PauseAnimation { duration: 400 } // Wait for main slide to be visible
NumberAnimation {
duration: 500
easing.type: Easing.OutQuart
}
}
}
}
opacity: isVisible ? 1.0 : 0.0
Behavior on opacity {
SequentialAnimation {
PauseAnimation { duration: 500 } // Wait for main slide
NumberAnimation {
duration: 500
easing.type: Easing.OutCubic
}
}
}
// Current time
Text {
id: timeText
font.family: "Roboto"
font.pixelSize: 84
font.weight: Font.ExtraLight
color: Data.ThemeManager.brightText
text: Qt.formatTime(new Date(), "hh:mm")
}
// Current date
Text {
id: dateText
font.family: "Roboto"
font.pixelSize: 28
font.weight: Font.Light
color: Data.ThemeManager.secondaryText
text: Qt.formatDate(new Date(), "dddd, MMMM d, yyyy")
}
// Time update timer
Timer {
id: timeTimer
interval: 1000
running: isVisible
repeat: true
onTriggered: {
timeText.text = Qt.formatTime(new Date(), "hh:mm")
dateText.text = Qt.formatDate(new Date(), "dddd, MMMM d, yyyy")
}
}
}

View file

@ -0,0 +1,367 @@
// System notification manager
import QtQuick
import QtQuick.Controls
import Quickshell.Services.Notifications
import "root:/Data" as Data
import "root:/Core" as Core
Item {
id: root
required property var shell
required property var notificationServer
// Dynamic height based on visible notifications
property int calculatedHeight: Math.min(notifications.length, maxNotifications) * 100 + 100 // Add 100px for bottom margin
// Simple array to store notifications with tracking
property var notifications: []
property int maxNotifications: 5
property var animatedNotificationIds: ({}) // Track which notifications have been animated
// Handle new notifications
Connections {
target: notificationServer
function onNotification(notification) {
if (!notification || !notification.id) return
// Filter empty notifications
if (!notification.appName && !notification.summary && !notification.body) {
return
}
// Filter ignored applications (case-insensitive) - same logic as NotificationService
var shouldIgnore = false
if (notification.appName && Data.Settings.ignoredApps && Data.Settings.ignoredApps.length > 0) {
for (var i = 0; i < Data.Settings.ignoredApps.length; i++) {
if (Data.Settings.ignoredApps[i].toLowerCase() === notification.appName.toLowerCase()) {
shouldIgnore = true
break
}
}
}
if (shouldIgnore) {
return // Don't display ignored notifications
}
// Create simple notification object
let newNotification = {
"id": notification.id,
"appName": notification.appName || "App",
"summary": notification.summary || "",
"body": notification.body || "",
"timestamp": Date.now(),
"shouldSlideOut": false,
"icon": notification.icon || notification.image || notification.appIcon || "",
"rawNotification": notification // Keep reference to original
}
// Add to beginning
notifications.unshift(newNotification)
// Trigger model update first to let new notification animate
notificationsChanged()
// Delay trimming to let new notification animate
if (notifications.length > maxNotifications) {
trimTimer.restart()
}
}
}
// Timer to delay trimming notifications (let new ones animate first)
Timer {
id: trimTimer
interval: 500 // Wait 500ms before trimming
running: false
repeat: false
onTriggered: {
if (notifications.length > maxNotifications) {
notifications = notifications.slice(0, maxNotifications)
notificationsChanged()
}
}
}
// Global timer to check for expired notifications
Timer {
id: cleanupTimer
interval: Math.min(500, Data.Settings.displayTime / 10) // Check every 500ms or 1/10th of display time, whichever is shorter
running: true
repeat: true
onTriggered: {
let currentTime = Date.now()
let hasExpiredNotifications = false
// Mark notifications older than displayTime setting for slide-out
for (let i = 0; i < notifications.length; i++) {
let notification = notifications[i]
let age = currentTime - notification.timestamp
if (age >= Data.Settings.displayTime && !notification.shouldSlideOut) {
notification.shouldSlideOut = true
hasExpiredNotifications = true
}
}
// Trigger update if any notifications were marked for slide-out
if (hasExpiredNotifications) {
notificationsChanged()
}
}
}
function removeNotification(notificationId) {
let initialLength = notifications.length
notifications = notifications.filter(function(n) { return n.id !== notificationId })
if (notifications.length !== initialLength) {
// Remove from animated tracking
delete animatedNotificationIds[notificationId]
notificationsChanged()
}
}
// Simple Column with Repeater
Column {
anchors.right: parent.right
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.leftMargin: 40 // Create space on left for top-left corner
anchors.rightMargin: Data.Settings.borderWidth + 20 // Border width plus corner space
anchors.bottomMargin: 100 // Create more space at bottom for bottom corner
spacing: 0
Repeater {
model: notifications.length // Show all notifications, not just maxNotifications
delegate: Rectangle {
id: notificationRect
property var notification: index < notifications.length ? notifications[index] : null
width: 400
height: 100
color: Data.ThemeManager.bgColor
// Only bottom visible notification gets bottom-left radius
radius: 0
bottomLeftRadius: index === Math.min(notifications.length, maxNotifications) - 1 && index < maxNotifications ? 15 : 0
// Only show if within maxNotifications limit
visible: index < maxNotifications
// Animation state
property bool hasSlideIn: false
// Check for expiration and start slide-out if needed
onNotificationChanged: {
if (notification && notification.shouldSlideOut && !slideOutAnimation.running) {
slideOutAnimation.start()
}
}
// Start off-screen for new notifications
Component.onCompleted: {
if (notification) {
// Check if notification should slide out immediately
if (notification.shouldSlideOut) {
slideOutAnimation.start()
return
}
// Check if this notification is truly new (recently added)
let notificationAge = Date.now() - notification.timestamp
let shouldAnimate = !animatedNotificationIds[notification.id] && notificationAge < 1000 // Only animate if less than 1 second old
if (shouldAnimate) {
x = 420
opacity = 0
hasSlideIn = false
slideInAnimation.start()
// Mark as animated
animatedNotificationIds[notification.id] = true
} else {
x = 0
opacity = 1
hasSlideIn = true
// Mark as animated if not already
animatedNotificationIds[notification.id] = true
}
}
}
// Slide-in animation
ParallelAnimation {
id: slideInAnimation
NumberAnimation {
target: notificationRect
property: "x"
to: 0
duration: 300
easing.type: Easing.OutCubic
}
NumberAnimation {
target: notificationRect
property: "opacity"
to: 1
duration: 300
easing.type: Easing.OutCubic
}
onFinished: {
hasSlideIn = true
}
}
// Slide-out animation
ParallelAnimation {
id: slideOutAnimation
NumberAnimation {
target: notificationRect
property: "x"
to: 420
duration: 250
easing.type: Easing.InCubic
}
NumberAnimation {
target: notificationRect
property: "opacity"
to: 0
duration: 250
easing.type: Easing.InCubic
}
onFinished: {
if (notification) {
removeNotification(notification.id)
}
}
}
// Click to dismiss
MouseArea {
anchors.fill: parent
onClicked: slideOutAnimation.start()
}
// Content
Row {
anchors.fill: parent
anchors.margins: 15
spacing: 12
// App icon
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(255, 255, 255, 0.1)
anchors.verticalCenter: parent.verticalCenter
// Application icon (if available)
Image {
id: appIcon
source: {
if (!notification || !notification.icon) return ""
let icon = notification.icon
// Apply same processing as tray system
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`;
}
// Handle file paths properly
if (icon.startsWith('/')) {
return "file://" + icon
}
return icon
}
anchors.fill: parent
anchors.margins: 2
fillMode: Image.PreserveAspectFit
smooth: true
visible: source.toString() !== ""
onStatusChanged: {
// Icon status handling can be added here if needed
}
}
// Fallback text (first letter of app name)
Text {
anchors.centerIn: parent
text: notification && notification.appName ? notification.appName.charAt(0).toUpperCase() : "!"
color: Data.ThemeManager.accentColor
font.family: "Roboto"
font.pixelSize: 16
font.bold: true
visible: !appIcon.visible
}
}
// Content
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 60
spacing: 4
Text {
text: notification ? notification.appName : ""
color: Data.ThemeManager.accentColor
font.family: "Roboto"
font.bold: true
font.pixelSize: 15
width: Math.min(parent.width, 250) // Earlier line break
elide: Text.ElideRight
}
Text {
text: notification ? notification.summary : ""
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 14
width: Math.min(parent.width, 250) // Earlier line break
wrapMode: Text.Wrap
maximumLineCount: 2
elide: Text.ElideRight
visible: text.length > 0
}
Text {
text: notification ? notification.body : ""
color: Qt.lighter(Data.ThemeManager.fgColor, 1.3)
font.family: "Roboto"
font.pixelSize: 13
width: Math.min(parent.width, 250) // Earlier line break
wrapMode: Text.Wrap
maximumLineCount: 2
elide: Text.ElideRight
visible: text.length > 0
}
}
}
// Top corner for first notification
Core.Corners {
position: "bottomright"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: -361
offsetY: -13
visible: index === 0 && index < maxNotifications
}
// Bottom corner for last visible notification
Core.Corners {
position: "bottomright"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: 39
offsetY: 78
visible: index === Math.min(notifications.length, maxNotifications) - 1 && index < maxNotifications
}
}
}
}
}

View file

@ -0,0 +1,263 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import "root:/Data" as Data
// Notification history viewer
Item {
id: root
implicitHeight: 400
required property var shell
property bool hovered: false
property real targetX: 0
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
// Header with title, count, and clear all button
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: 40
spacing: 8
Text {
text: "Notification History"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "Roboto"
}
Text {
text: "(" + (shell.notificationHistory ? shell.notificationHistory.count : 0) + ")"
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
opacity: 0.7
}
Item { Layout.fillWidth: true }
Rectangle {
visible: shell.notificationHistory && shell.notificationHistory.count > 0
width: clearText.implicitWidth + 16
height: 24
radius: 12
color: clearMouseArea.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : "transparent"
border.color: Data.ThemeManager.accentColor
border.width: 1
Text {
id: clearText
anchors.centerIn: parent
text: "Clear All"
color: Data.ThemeManager.accentColor
font.family: "Roboto"
font.pixelSize: 11
}
MouseArea {
id: clearMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: shell.notificationHistory.clear()
}
}
}
// Scrollable notification list
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ScrollView {
id: scrollView
anchors.fill: parent
clip: true
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
interactive: true
visible: notificationListView.contentHeight > notificationListView.height
contentItem: Rectangle {
implicitWidth: 6
radius: width / 2
color: parent.pressed ? Data.ThemeManager.accentColor
: parent.hovered ? Qt.lighter(Data.ThemeManager.accentColor, 1.2)
: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.7)
}
}
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ListView {
id: notificationListView
model: shell.notificationHistory
spacing: 12
cacheBuffer: 50 // Memory optimization
reuseItems: true
boundsBehavior: Flickable.StopAtBounds
maximumFlickVelocity: 2500
flickDeceleration: 1500
clip: true
interactive: true
// Smooth scrolling behavior
property real targetY: contentY
Behavior on targetY {
NumberAnimation {
duration: 200
easing.type: Easing.OutQuad
}
}
onTargetYChanged: {
if (!moving && !dragging) {
contentY = targetY
}
}
delegate: Rectangle {
width: notificationListView.width
height: Math.max(80, contentLayout.implicitHeight + 24)
radius: 8
color: mouseArea.containsMouse ? Qt.darker(Data.ThemeManager.bgColor, 1.15) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: Data.ThemeManager.accentColor
border.width: 1
// View optimization - only render visible items
visible: y + height > notificationListView.contentY - height &&
y < notificationListView.contentY + notificationListView.height + height
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
}
// Main notification content layout
RowLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: 12
spacing: 12
// App icon area
Item {
Layout.preferredWidth: 24
Layout.preferredHeight: 24
Layout.alignment: Qt.AlignTop
Image {
width: 24
height: 24
source: model.icon || ""
visible: source.toString() !== ""
}
}
// Notification text content
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 6
// App name and timestamp row
RowLayout {
Layout.fillWidth: true
Text {
Layout.fillWidth: true
text: model.appName || "Unknown"
color: Data.ThemeManager.accentColor
font.family: "Roboto"
font.pixelSize: 13
font.bold: true
}
Text {
text: Qt.formatDateTime(model.timestamp, "hh:mm")
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 10
opacity: 0.7
}
}
// Notification summary
Text {
Layout.fillWidth: true
visible: model.summary && model.summary.length > 0
text: model.summary || ""
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 13
font.bold: true
wrapMode: Text.WordWrap
lineHeight: 1.2
}
// Notification body text
Text {
Layout.fillWidth: true
visible: model.body && model.body.length > 0
text: model.body || ""
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
opacity: 0.9
wrapMode: Text.WordWrap
maximumLineCount: 4
elide: Text.ElideRight
lineHeight: 1.2
}
}
}
// Individual delete button
Rectangle {
width: 24
height: 24
radius: 12
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 8
color: deleteArea.containsMouse ? Qt.rgba(255, 0, 0, 0.2) : "transparent"
border.color: deleteArea.containsMouse ? "#ff4444" : Data.ThemeManager.fgColor
border.width: 1
opacity: deleteArea.containsMouse ? 1 : 0.5
Text {
anchors.centerIn: parent
text: "×"
color: deleteArea.containsMouse ? "#ff4444" : Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 16
}
MouseArea {
id: deleteArea
anchors.fill: parent
hoverEnabled: true
onClicked: shell.notificationHistory.remove(model.index)
}
}
}
}
}
// Empty state message
Text {
anchors.centerIn: parent
visible: !notificationListView.count
text: "No notifications"
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 14
opacity: 0.7
}
}
}
}

View file

@ -0,0 +1,143 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import "root:/Data" as Data
import "root:/Core/" as Core
import "./modules" as Modules
// Top panel wrapper with recording
Item {
id: topPanelRoot
required property var shell
visible: true
property bool isRecording: false
property var recordingProcess: null
property string lastError: ""
property bool wallpaperSelectorVisible: false
signal slideBarVisibilityChanged(bool visible)
function triggerTopPanel() {
panel.show()
}
// Auto-trigger panel
onVisibleChanged: {
if (visible) {
triggerTopPanel()
}
}
// Main panel instance
Modules.Panel {
id: panel
shell: topPanelRoot.shell
isRecording: topPanelRoot.isRecording
anchors.top: topPanelRoot.top
anchors.right: topPanelRoot.right
anchors.topMargin: 8
anchors.rightMargin: 8
onVisibleChanged: slideBarVisibilityChanged(visible)
onRecordingRequested: startRecording()
onStopRecordingRequested: {
stopRecording()
// Hide entire TopPanel after stop recording
if (topPanelRoot.parent && topPanelRoot.parent.hide) {
topPanelRoot.parent.hide()
}
}
onSystemActionRequested: function(action) {
performSystemAction(action)
// Hide entire TopPanel after system action
if (topPanelRoot.parent && topPanelRoot.parent.hide) {
topPanelRoot.parent.hide()
}
}
onPerformanceActionRequested: function(action) {
performPerformanceAction(action)
// Hide entire TopPanel after performance action
if (topPanelRoot.parent && topPanelRoot.parent.hide) {
topPanelRoot.parent.hide()
}
}
}
// Start screen recording
function startRecording() {
var currentDate = new Date()
var hours = String(currentDate.getHours()).padStart(2, '0')
var minutes = String(currentDate.getMinutes()).padStart(2, '0')
var day = String(currentDate.getDate()).padStart(2, '0')
var month = String(currentDate.getMonth() + 1).padStart(2, '0')
var year = currentDate.getFullYear()
var filename = hours + "-" + minutes + "-" + day + "-" + month + "-" + year + ".mp4"
var outputPath = Data.Settings.videoPath + filename
var command = "gpu-screen-recorder -w portal -f 60 -a default_output -o " + outputPath
var qmlString = 'import Quickshell.Io; Process { command: ["sh", "-c", "' + command + '"]; running: true }'
recordingProcess = Qt.createQmlObject(qmlString, topPanelRoot)
isRecording = true
}
// Stop recording with cleanup
function stopRecording() {
if (recordingProcess && isRecording) {
var stopQmlString = 'import Quickshell.Io; Process { command: ["sh", "-c", "pkill -SIGINT -f \'gpu-screen-recorder.*portal\'"]; running: true; onExited: function() { destroy() } }'
var stopProcess = Qt.createQmlObject(stopQmlString, topPanelRoot)
var cleanupTimer = Qt.createQmlObject('import QtQuick; Timer { interval: 3000; running: true; repeat: false }', topPanelRoot)
cleanupTimer.triggered.connect(function() {
if (recordingProcess) {
recordingProcess.running = false
recordingProcess.destroy()
recordingProcess = null
}
var forceKillQml = 'import Quickshell.Io; Process { command: ["sh", "-c", "pkill -9 -f \'gpu-screen-recorder.*portal\' 2>/dev/null || true"]; running: true; onExited: function() { destroy() } }'
var forceKillProcess = Qt.createQmlObject(forceKillQml, topPanelRoot)
cleanupTimer.destroy()
})
}
isRecording = false
}
// System action router (lock, reboot, shutdown)
function performSystemAction(action) {
switch(action) {
case "lock":
Core.ProcessManager.lock()
break
case "reboot":
Core.ProcessManager.reboot()
break
case "shutdown":
Core.ProcessManager.shutdown()
break
}
}
function performPerformanceAction(action) {
// Performance actions handled silently
}
// Clean up processes on destruction
Component.onDestruction: {
if (recordingProcess) {
recordingProcess.running = false
recordingProcess.destroy()
recordingProcess = null
}
}
}

View file

@ -0,0 +1,72 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// Calendar button for the top panel
Rectangle {
id: calendarButton
width: 40
height: 80
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
property bool containsMouse: calendarMouseArea.containsMouse
property bool calendarVisible: false
property var calendarPopup: null
property var shell: null // Shell reference from parent
signal entered()
signal exited()
onContainsMouseChanged: {
if (containsMouse) {
entered()
} else {
exited()
}
}
MouseArea {
id: calendarMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
toggleCalendar()
}
}
Label {
anchors.centerIn: parent
text: "calendar_month"
font.pixelSize: 24
font.family: "Material Symbols Outlined"
color: calendarButton.containsMouse || calendarButton.calendarVisible ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
function toggleCalendar() {
if (!calendarPopup) {
var component = Qt.createComponent("root:/Widgets/Calendar/CalendarPopup.qml")
if (component.status === Component.Ready) {
calendarPopup = component.createObject(calendarButton.parent, {
"targetX": calendarButton.x + calendarButton.width + 10,
"shell": calendarButton.shell
})
} else if (component.status === Component.Error) {
console.log("Error loading calendar:", component.errorString())
return
}
}
if (calendarPopup) {
calendarVisible = !calendarVisible
calendarPopup.setClickMode(calendarVisible)
}
}
function hideCalendar() {
if (calendarPopup) {
calendarVisible = false
calendarPopup.setClickMode(false)
}
}
}

View file

@ -0,0 +1,103 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
Row {
id: root
spacing: 16
visible: true
height: 80
required property bool isRecording
required property var shell
signal performanceActionRequested(string action)
signal systemActionRequested(string action)
signal mouseChanged(bool containsMouse)
// Add hover tracking property
property bool containsMouse: performanceSection.containsMouse || systemSection.containsMouse
onContainsMouseChanged: mouseChanged(containsMouse)
Rectangle {
id: performanceSection
width: (parent.width - parent.spacing) / 2
height: parent.height
radius: 20
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
visible: true
// Add hover tracking for performance section
property bool containsMouse: performanceMouseArea.containsMouse || performanceControls.containsMouse
MouseArea {
id: performanceMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onContainsMouseChanged: {
if (containsMouse) {
performanceSection.containsMouse = true
} else if (!performanceControls.containsMouse) {
performanceSection.containsMouse = false
}
}
}
PerformanceControls {
id: performanceControls
anchors.fill: parent
anchors.margins: 12
shell: root.shell
onPerformanceActionRequested: function(action) { root.performanceActionRequested(action) }
onMouseChanged: function(containsMouse) {
if (containsMouse) {
performanceSection.containsMouse = true
} else if (!performanceMouseArea.containsMouse) {
performanceSection.containsMouse = false
}
}
}
}
Rectangle {
id: systemSection
width: (parent.width - parent.spacing) / 2
height: parent.height
radius: 20
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
visible: true
// Add hover tracking for system section
property bool containsMouse: systemMouseArea.containsMouse || systemControls.containsMouse
MouseArea {
id: systemMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onContainsMouseChanged: {
if (containsMouse) {
systemSection.containsMouse = true
} else if (!systemControls.containsMouse) {
systemSection.containsMouse = false
}
}
}
SystemControls {
id: systemControls
anchors.fill: parent
anchors.margins: 12
shell: root.shell
onSystemActionRequested: function(action) { root.systemActionRequested(action) }
onMouseChanged: function(containsMouse) {
if (containsMouse) {
systemSection.containsMouse = true
} else if (!systemMouseArea.containsMouse) {
systemSection.containsMouse = false
}
}
}
}
}

View file

@ -0,0 +1,98 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
Rectangle {
id: root
width: 42
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 12
z: 2 // Keep it above notification history
required property bool notificationHistoryVisible
required property bool clipboardHistoryVisible
required property var notificationHistory
signal notificationToggleRequested()
signal clipboardToggleRequested()
// Add containsMouse property for panel hover tracking
property bool containsMouse: notifButtonMouseArea.containsMouse || clipButtonMouseArea.containsMouse
// Ensure minimum height for buttons even when recording
property real buttonHeight: 38
height: buttonHeight * 2 + 4 // 4px spacing between buttons
Item {
anchors.fill: parent
anchors.margins: 2
// Top pill (Notifications)
Rectangle {
id: notificationPill
anchors {
top: parent.top
left: parent.left
right: parent.right
bottom: parent.verticalCenter
bottomMargin: 2 // Half of the spacing
}
radius: 12
color: notifButtonMouseArea.containsMouse || root.notificationHistoryVisible ?
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.05)
border.color: notifButtonMouseArea.containsMouse || root.notificationHistoryVisible ? Data.ThemeManager.accentColor : "transparent"
border.width: 1
MouseArea {
id: notifButtonMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: root.notificationToggleRequested()
}
Label {
anchors.centerIn: parent
text: "notifications"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: notifButtonMouseArea.containsMouse || root.notificationHistoryVisible ?
Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
}
// Bottom pill (Clipboard)
Rectangle {
id: clipboardPill
anchors {
top: parent.verticalCenter
left: parent.left
right: parent.right
bottom: parent.bottom
topMargin: 2 // Half of the spacing
}
radius: 12
color: clipButtonMouseArea.containsMouse || root.clipboardHistoryVisible ?
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.05)
border.color: clipButtonMouseArea.containsMouse || root.clipboardHistoryVisible ? Data.ThemeManager.accentColor : "transparent"
border.width: 1
MouseArea {
id: clipButtonMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: root.clipboardToggleRequested()
}
Label {
anchors.centerIn: parent
text: "content_paste"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: clipButtonMouseArea.containsMouse || root.clipboardHistoryVisible ?
Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
}
}
}

View file

@ -0,0 +1,704 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Services.SystemTray
import "root:/Data" as Data
import "root:/Core" as Core
import "root:/Widgets/System" as System
import "root:/Widgets/Notifications" as Notifications
import "." as Modules
// Main tabbed panel
Item {
id: root
// Size calculation
width: mainContainer.implicitWidth + 18
height: mainContainer.implicitHeight + 18
required property var shell
property bool isShown: false
property int currentTab: 0 // 0=main, 1=calendar, 2=clipboard, 3=notifications, 4=wallpapers
property real bgOpacity: 0.0
property bool isRecording: false
property var tabIcons: ["widgets", "calendar_month", "content_paste", "notifications", "wallpaper"]
signal recordingRequested()
signal stopRecordingRequested()
signal systemActionRequested(string action)
signal performanceActionRequested(string action)
// Animation state management
visible: opacity > 0
opacity: 0
x: width
property var tabNames: ["Main", "Calendar", "Clipboard", "Notifications", "Wallpapers"]
Behavior on opacity {
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
}
Behavior on x {
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
}
// Background with bottom-only rounded corners
Rectangle {
anchors.fill: parent
color: Data.ThemeManager.bgColor
topLeftRadius: 0
topRightRadius: 0
bottomLeftRadius: 20
bottomRightRadius: 20
}
// Shadow effect preparation
Rectangle {
id: shadowSource
anchors.fill: mainContainer
color: "transparent"
visible: false
bottomLeftRadius: 20
bottomRightRadius: 20
}
DropShadow {
anchors.fill: shadowSource
horizontalOffset: 0
verticalOffset: 2
radius: 8.0
samples: 17
color: "#80000000"
source: shadowSource
z: 1
}
// Main container with tab-based content layout
Rectangle {
id: mainContainer
anchors.fill: parent
anchors.margins: 9
color: "transparent"
radius: 12
implicitWidth: 600 // Fixed width for consistency
implicitHeight: 360
Behavior on height {
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
}
MouseArea {
id: backgroundMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
}
// Left sidebar with tab navigation
Item {
id: tabSidebar
width: 40
height: parent.height
anchors.left: parent.left
anchors.leftMargin: 9
anchors.top: parent.top
anchors.topMargin: 54
property bool containsMouse: sidebarMouseArea.containsMouse || tabColumn.containsMouse
// Tab button background
Rectangle {
width: 36
height: tabColumn.height + 8
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
color: Qt.darker(Data.ThemeManager.bgColor, 1.05)
radius: 18
border.color: Qt.darker(Data.ThemeManager.bgColor, 1.2)
border.width: 1
}
MouseArea {
id: sidebarMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onEntered: hideTimer.stop()
onExited: {
if (!root.isHovered) {
hideTimer.restart()
}
}
}
// Tab icon buttons
Column {
id: tabColumn
spacing: 4
anchors.top: parent.top
anchors.topMargin: 4
anchors.horizontalCenter: parent.horizontalCenter
property bool containsMouse: {
for (let i = 0; i < tabRepeater.count; i++) {
let tab = tabRepeater.itemAt(i)
if (tab && tab.children[0] && tab.children[0].containsMouse) {
return true
}
}
return false
}
Repeater {
id: tabRepeater
model: 5
delegate: Rectangle {
width: 32
height: 32
radius: 16
color: currentTab === index ? Data.ThemeManager.accentColor : Qt.darker(Data.ThemeManager.bgColor, 1.15)
property bool isHovered: tabMouseArea.containsMouse
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: root.currentTab = index
onEntered: hideTimer.stop()
onExited: {
if (!root.isHovered) {
hideTimer.restart()
}
}
}
Text {
anchors.centerIn: parent
text: root.tabIcons[index]
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: currentTab === index ? Data.ThemeManager.bgColor :
parent.isHovered ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
}
}
}
}
// Main content area (positioned right of tab sidebar)
Column {
id: mainColumn
width: parent.width - tabSidebar.width - 45
anchors.left: tabSidebar.right
anchors.leftMargin: 9
anchors.top: parent.top
anchors.margins: 18
spacing: 28
clip: true
// Tab 0: Main dashboard content
Column {
width: parent.width
spacing: 28
visible: root.currentTab === 0
// User profile row with theme toggle and weather
Row {
width: parent.width
spacing: 18
UserProfile {
id: userProfile
width: parent.width - themeToggle.width - weatherDisplay.width - (parent.spacing * 2)
height: 80
shell: root.shell
}
ThemeToggle {
id: themeToggle
width: 40
height: userProfile.height
}
WeatherDisplay {
id: weatherDisplay
width: parent.width * 0.18
height: userProfile.height
shell: root.shell
onEntered: hideTimer.stop()
onExited: hideTimer.restart()
visible: root.visible
enabled: visible
}
}
// Controls section
Row {
width: parent.width
spacing: 18
Column {
width: parent.width
spacing: 28
RecordingButton {
id: recordingButton
width: parent.width
height: 48
shell: root.shell
isRecording: root.isRecording
onRecordingRequested: root.recordingRequested()
onStopRecordingRequested: root.stopRecordingRequested()
}
Controls {
id: controls
width: parent.width
isRecording: root.isRecording
shell: root.shell
onPerformanceActionRequested: function(action) { root.performanceActionRequested(action) }
onSystemActionRequested: function(action) { root.systemActionRequested(action) }
onMouseChanged: function(containsMouse) {
if (containsMouse) {
hideTimer.stop()
} else if (!root.isHovered) {
hideTimer.restart()
}
}
}
}
}
// System tray section with inline menu
Column {
id: systemTraySection
width: parent.width
spacing: 8
property bool containsMouse: trayMouseArea.containsMouse || systemTrayModule.containsMouse
Rectangle {
id: trayBackground
width: parent.width
height: 40
radius: 20
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
property bool isActive: false
MouseArea {
id: trayMouseArea
anchors.fill: parent
anchors.margins: -10
hoverEnabled: true
propagateComposedEvents: true
preventStealing: false
onEntered: trayBackground.isActive = true
onExited: {
if (!inlineTrayMenu.visible) {
Qt.callLater(function() {
if (!systemTrayModule.containsMouse) {
trayBackground.isActive = false
}
})
}
}
}
System.SystemTray {
id: systemTrayModule
anchors.centerIn: parent
shell: root.shell
bar: parent
trayMenu: inlineTrayMenu
}
}
}
TrayMenu {
id: inlineTrayMenu
parent: mainContainer
width: parent.width
menu: null
systemTrayY: systemTraySection.y
systemTrayHeight: systemTraySection.height
onHideRequested: trayBackground.isActive = false
}
}
// Tab 1: Calendar content with lazy loading
Column {
width: parent.width
height: 310
visible: root.currentTab === 1
spacing: 16
Text {
text: "Calendar"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "FiraCode Nerd Font"
}
Rectangle {
width: parent.width
height: parent.height - parent.children[0].height - parent.spacing
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
radius: 20
clip: true
Loader {
anchors.fill: parent
anchors.margins: 20
active: visible && root.currentTab === 1
source: active ? "root:/Widgets/Calendar/Calendar.qml" : ""
onLoaded: {
if (item) {
item.shell = root.shell
}
}
}
}
}
// Tab 2: Clipboard history with clear button
Column {
width: parent.width
height: 310
visible: root.currentTab === 2
spacing: 16
RowLayout {
width: parent.width
spacing: 16
Text {
text: "Clipboard History"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "Roboto"
}
Item { Layout.fillWidth: true }
Rectangle {
width: clearClipText.implicitWidth + 16
height: 24
radius: 12
color: clearClipMouseArea.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : "transparent"
border.color: Data.ThemeManager.accentColor
border.width: 1
Text {
id: clearClipText
anchors.centerIn: parent
text: "Clear All"
color: Data.ThemeManager.accentColor
font.family: "Roboto"
font.pixelSize: 11
}
MouseArea {
id: clearClipMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
// Navigate to clipboard component and call clear
let clipLoader = parent.parent.parent.children[1].children[0]
if (clipLoader && clipLoader.item && clipLoader.item.children[0]) {
let clipComponent = clipLoader.item.children[0]
if (clipComponent.clearClipboardHistory) {
clipComponent.clearClipboardHistory()
}
}
}
}
}
}
Rectangle {
width: parent.width
height: parent.height - parent.children[0].height - parent.spacing
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
radius: 20
clip: true
Loader {
anchors.fill: parent
anchors.margins: 20
active: visible && root.currentTab === 2
sourceComponent: active ? clipboardHistoryComponent : null
onLoaded: {
if (item && item.children[0]) {
item.children[0].refreshClipboardHistory()
}
}
}
}
}
// Tab 3: Notification history with clear button
Column {
width: parent.width
height: 310
visible: root.currentTab === 3
spacing: 16
RowLayout {
width: parent.width
spacing: 16
Text {
text: "Notification History"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "Roboto"
}
Text {
text: "(" + (root.shell.notificationHistory ? root.shell.notificationHistory.count : 0) + ")"
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
opacity: 0.7
Layout.alignment: Qt.AlignVCenter
}
Item { Layout.fillWidth: true }
Rectangle {
width: clearNotifText.implicitWidth + 16
height: 24
radius: 12
color: clearNotifMouseArea.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : "transparent"
border.color: Data.ThemeManager.accentColor
border.width: 1
Text {
id: clearNotifText
anchors.centerIn: parent
text: "Clear All"
color: Data.ThemeManager.accentColor
font.family: "Roboto"
font.pixelSize: 11
}
MouseArea {
id: clearNotifMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: root.shell.notificationHistory.clear()
}
}
}
Rectangle {
width: parent.width
height: parent.height - parent.children[0].height - parent.spacing
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
radius: 20
clip: true
Loader {
anchors.fill: parent
anchors.margins: 20
active: visible && root.currentTab === 3
sourceComponent: active ? notificationHistoryComponent : null
}
}
}
// Tab 4: Wallpaper selector
Column {
width: parent.width
height: 310
visible: root.currentTab === 4
spacing: 16
Text {
text: "Wallpapers"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "Roboto"
}
Rectangle {
width: parent.width
height: parent.height - parent.children[0].height - parent.spacing
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
radius: 20
clip: true
Loader {
anchors.fill: parent
anchors.margins: 20
active: visible && root.currentTab === 4
sourceComponent: active ? wallpaperSelectorComponent : null
}
}
}
}
}
// Lazy-loaded components for tab content
Component {
id: clipboardHistoryComponent
Item {
anchors.fill: parent
System.Cliphist {
id: cliphistComponent
anchors.fill: parent
shell: root.shell
// Hide built-in header (we provide our own)
Component.onCompleted: {
for (let i = 0; i < children.length; i++) {
let child = children[i]
if (child.objectName === "contentColumn" || child.toString().includes("ColumnLayout")) {
if (child.children && child.children.length > 0) {
child.children[0].visible = false
}
}
}
}
}
}
}
Component {
id: notificationHistoryComponent
Item {
anchors.fill: parent
Notifications.NotificationHistory {
anchors.fill: parent
shell: root.shell
clip: true
// Hide built-in header (we provide our own)
Component.onCompleted: {
for (let i = 0; i < children.length; i++) {
let child = children[i]
if (child.objectName === "contentColumn" || child.toString().includes("ColumnLayout")) {
if (child.children && child.children.length > 0) {
child.children[0].visible = false
}
}
}
}
}
}
}
Component {
id: wallpaperSelectorComponent
Modules.WallpaperSelector {
isVisible: parent && parent.parent && parent.parent.visible
}
}
// Complex hover state calculation for auto-hide behavior
property bool isHovered: {
const menuStates = {
inlineMenuActive: inlineTrayMenu.menuJustOpened || inlineTrayMenu.visible,
trayActive: trayBackground.isActive,
tabContentActive: currentTab !== 0
}
if (menuStates.inlineMenuActive || menuStates.trayActive || menuStates.tabContentActive) return true
const mouseStates = {
backgroundHovered: backgroundMouseArea.containsMouse,
recordingHovered: recordingButton.containsMouse,
controlsHovered: controls.containsMouse,
profileHovered: userProfile.isHovered,
themeToggleHovered: themeToggle.containsMouse,
systemTrayHovered: systemTraySection.containsMouse ||
trayMouseArea.containsMouse ||
systemTrayModule.containsMouse,
menuHovered: inlineTrayMenu.containsMouse,
weatherHovered: weatherDisplay.containsMouse,
tabSidebarHovered: tabSidebar.containsMouse,
mainContentHovered: mainColumn.children[0].visible && backgroundMouseArea.containsMouse
}
return Object.values(mouseStates).some(state => state)
}
// Auto-hide timer
Timer {
id: hideTimer
interval: 500
repeat: false
onTriggered: hide()
}
onIsHoveredChanged: {
if (isHovered) {
hideTimer.stop()
} else if (!inlineTrayMenu.visible && !trayBackground.isActive && !tabSidebar.containsMouse && !tabColumn.containsMouse) {
hideTimer.restart()
}
}
function show() {
if (isShown) return
isShown = true
hideTimer.stop()
opacity = 1
x = 0
}
function hide() {
if (!isShown || inlineTrayMenu.menuJustOpened || inlineTrayMenu.visible) return
// Only hide on main tab when nothing is hovered
if (currentTab === 0 && !isHovered) {
isShown = false
x = width
opacity = 0
// Hide parent TopPanel as well
if (parent && parent.parent && parent.parent.hide) {
parent.parent.hide()
}
}
}
Component.onCompleted: {
Qt.callLater(function() {
mainColumn.visible = true
})
}
// Border integration corners
Core.Corners {
id: topLeftCorner
position: "bottomright"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: 0
offsetY: 0
}
Core.Corners {
id: topRightCorner
position: "bottomleft"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: root.width
offsetY: 0
}
}

View file

@ -0,0 +1,127 @@
import QtQuick
import QtQuick.Controls
import Quickshell.Services.UPower
Column {
id: root
required property var shell
spacing: 8
signal performanceActionRequested(string action)
signal mouseChanged(bool containsMouse)
readonly property bool containsMouse: performanceButton.containsMouse ||
balancedButton.containsMouse ||
powerSaverButton.containsMouse
// Safe property access with fallbacks
readonly property bool upowerReady: typeof PowerProfiles !== 'undefined' && PowerProfiles
readonly property int currentProfile: upowerReady ? PowerProfiles.profile : 0
onContainsMouseChanged: root.mouseChanged(containsMouse)
opacity: visible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
Row {
spacing: 8
width: parent.width
// Performance Profile Button
SystemButton {
id: performanceButton
width: (parent.width - parent.spacing * 2) / 3
height: 52
shell: root.shell
iconText: "speed"
isActive: root.upowerReady && (typeof PowerProfile !== 'undefined') ?
root.currentProfile === PowerProfile.Performance : false
onClicked: {
if (root.upowerReady && typeof PowerProfile !== 'undefined') {
PowerProfiles.profile = PowerProfile.Performance
root.performanceActionRequested("performance")
} else {
console.warn("PowerProfiles not available")
}
}
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
// Balanced Profile Button
SystemButton {
id: balancedButton
width: (parent.width - parent.spacing * 2) / 3
height: 52
shell: root.shell
iconText: "balance"
isActive: root.upowerReady && (typeof PowerProfile !== 'undefined') ?
root.currentProfile === PowerProfile.Balanced : false
onClicked: {
if (root.upowerReady && typeof PowerProfile !== 'undefined') {
PowerProfiles.profile = PowerProfile.Balanced
root.performanceActionRequested("balanced")
} else {
console.warn("PowerProfiles not available")
}
}
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
// Power Saver Profile Button
SystemButton {
id: powerSaverButton
width: (parent.width - parent.spacing * 2) / 3
height: 52
shell: root.shell
iconText: "battery_saver"
isActive: root.upowerReady && (typeof PowerProfile !== 'undefined') ?
root.currentProfile === PowerProfile.PowerSaver : false
onClicked: {
if (root.upowerReady && typeof PowerProfile !== 'undefined') {
PowerProfiles.profile = PowerProfile.PowerSaver
root.performanceActionRequested("powersaver")
} else {
console.warn("PowerProfiles not available")
}
}
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
}
// Optional: Add a small delay to ensure services are ready
Component.onCompleted: {
// Small delay to ensure UPower service is fully initialized
Qt.callLater(function() {
if (!root.upowerReady) {
console.warn("UPower service not ready - performance controls may not work correctly")
}
})
}
}

View file

@ -0,0 +1,61 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import "root:/Data" as Data
Rectangle {
id: root
required property var shell
required property bool isRecording
radius: 20
signal recordingRequested()
signal stopRecordingRequested()
signal mouseChanged(bool containsMouse)
// Gray by default, accent color on hover or when recording
color: isRecording ? Data.ThemeManager.accentColor :
(mouseArea.containsMouse ? Data.ThemeManager.accentColor : Qt.darker(Data.ThemeManager.bgColor, 1.15))
property bool isHovered: mouseArea.containsMouse
readonly property alias containsMouse: mouseArea.containsMouse
RowLayout {
anchors.centerIn: parent
spacing: 10
Text {
text: isRecording ? "stop_circle" : "radio_button_unchecked"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: isRecording || mouseArea.containsMouse ? "#ffffff" : Data.ThemeManager.fgColor
Layout.alignment: Qt.AlignVCenter
}
Label {
text: isRecording ? "Stop Recording" : "Start Recording"
font.pixelSize: 13
font.weight: Font.Medium
color: isRecording || mouseArea.containsMouse ? "#ffffff" : Data.ThemeManager.fgColor
Layout.alignment: Qt.AlignVCenter
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onContainsMouseChanged: root.mouseChanged(containsMouse)
onClicked: {
if (isRecording) {
root.stopRecordingRequested()
} else {
root.recordingRequested()
}
}
}
}

View file

@ -0,0 +1,116 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
Rectangle {
id: root
required property var shell
required property string iconText
property string labelText: ""
// Add active state property
property bool isActive: false
radius: 20
// Modified color logic to handle active state
color: {
if (isActive) {
return mouseArea.containsMouse ?
Qt.lighter(Data.ThemeManager.accentColor, 1.1) :
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
} else {
return mouseArea.containsMouse ?
Qt.lighter(Data.ThemeManager.accentColor, 1.2) :
Qt.lighter(Data.ThemeManager.bgColor, 1.15)
}
}
border.width: isActive ? 2 : 1
border.color: isActive ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.3)
signal clicked()
signal mouseChanged(bool containsMouse)
property bool isHovered: mouseArea.containsMouse
readonly property alias containsMouse: mouseArea.containsMouse
Behavior on color {
ColorAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
Behavior on border.color {
ColorAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
scale: isHovered ? 1.05 : 1.0
Behavior on scale {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
Column {
anchors.centerIn: parent
spacing: 2
Text {
text: root.iconText
font.family: "Material Symbols Outlined"
font.pixelSize: 16
anchors.horizontalCenter: parent.horizontalCenter
color: {
if (root.isActive) {
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
} else {
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
}
}
Behavior on color {
ColorAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
Label {
text: root.labelText
font.family: "Roboto"
font.pixelSize: 8
color: {
if (root.isActive) {
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
} else {
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
}
}
anchors.horizontalCenter: parent.horizontalCenter
font.weight: root.isActive ? Font.Bold : Font.Medium
visible: root.labelText !== ""
Behavior on color {
ColorAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onContainsMouseChanged: root.mouseChanged(containsMouse)
onClicked: root.clicked()
}
}

View file

@ -0,0 +1,92 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
RowLayout {
id: root
required property var shell
spacing: 8
signal systemActionRequested(string action)
signal mouseChanged(bool containsMouse)
readonly property bool containsMouse: lockButton.containsMouse ||
rebootButton.containsMouse ||
shutdownButton.containsMouse
onContainsMouseChanged: root.mouseChanged(containsMouse)
opacity: visible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
// Lock Button
SystemButton {
id: lockButton
Layout.fillHeight: true
Layout.fillWidth: true
shell: root.shell
iconText: "lock"
onClicked: {
console.log("Lock button clicked")
console.log("root.shell:", root.shell)
console.log("root.shell.lockscreen:", root.shell ? root.shell.lockscreen : "shell is null")
// Directly trigger custom lockscreen
if (root.shell && root.shell.lockscreen) {
console.log("Calling root.shell.lockscreen.lock()")
root.shell.lockscreen.lock()
} else {
console.log("Fallback to systemActionRequested")
// Fallback to system action for compatibility
root.systemActionRequested("lock")
}
}
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
// Reboot Button
SystemButton {
id: rebootButton
Layout.fillHeight: true
Layout.fillWidth: true
shell: root.shell
iconText: "restart_alt"
onClicked: root.systemActionRequested("reboot")
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
// Shutdown Button
SystemButton {
id: shutdownButton
Layout.fillHeight: true
Layout.fillWidth: true
shell: root.shell
iconText: "power_settings_new"
onClicked: root.systemActionRequested("shutdown")
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
}

View file

@ -0,0 +1,42 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
Rectangle {
id: root
property var shell: null
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
property bool containsMouse: themeMouseArea.containsMouse
property bool menuJustOpened: false
signal entered()
signal exited()
onContainsMouseChanged: {
if (containsMouse) {
entered()
} else if (!menuJustOpened) {
exited()
}
}
MouseArea {
id: themeMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Data.ThemeManager.toggleTheme()
}
}
Label {
anchors.centerIn: parent
text: "contrast"
font.pixelSize: 24
font.family: "Material Symbols Outlined"
color: containsMouse ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
}

View file

@ -0,0 +1,48 @@
import QtQuick
Rectangle {
id: root
width: 360
height: 1
color: "red"
anchors.top: parent.top
signal triggered()
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
property bool isHovered: containsMouse
onIsHoveredChanged: {
if (isHovered) {
showTimer.start()
hideTimer.stop()
} else {
hideTimer.start()
showTimer.stop()
}
}
onEntered: hideTimer.stop()
}
// Smooth show/hide timers
Timer {
id: showTimer
interval: 200
onTriggered: root.triggered()
}
Timer {
id: hideTimer
interval: 500
}
// Exposed properties and functions
readonly property alias containsMouse: mouseArea.containsMouse
function stopHideTimer() { hideTimer.stop() }
function startHideTimer() { hideTimer.start() }
}

View file

@ -0,0 +1,212 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import "root:/Data" as Data
Rectangle {
id: root
width: parent.width
height: visible ? calculatedHeight : 0
visible: false
enabled: visible
clip: true
color: Data.ThemeManager.bgColor
radius: 20
required property var menu
required property var systemTrayY
required property var systemTrayHeight
property bool containsMouse: trayMenuMouseArea.containsMouse
property bool menuJustOpened: false
property point triggerPoint: Qt.point(0, 0)
property Item originalParent
property int totalCount: opener.children ? opener.children.values.length : 0
signal hideRequested()
MouseArea {
id: trayMenuMouseArea
anchors.fill: parent
hoverEnabled: true
preventStealing: true
propagateComposedEvents: false
}
onVisibleChanged: {
if (visible) {
menuJustOpened = true
Qt.callLater(function() {
menuJustOpened = false
})
}
}
function toggle() {
visible = !visible
if (visible) {
menuJustOpened = true
Qt.callLater(function() {
menuJustOpened = false
})
}
}
function show(point, parentItem) {
visible = true
menuJustOpened = true
Qt.callLater(function() {
menuJustOpened = false
})
}
function hide() {
visible = false
menuJustOpened = false
hideRequested()
}
y: {
var preferredY = systemTrayY + systemTrayHeight + 10
var availableSpace = parent.height - preferredY - 20
if (calculatedHeight > availableSpace) {
return systemTrayY - height - 10
}
return preferredY
}
Behavior on height {
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
}
property int calculatedHeight: {
if (totalCount === 0) return 40
var separatorCount = 0
var regularItemCount = 0
if (opener.children && opener.children.values) {
for (var i = 0; i < opener.children.values.length; i++) {
if (opener.children.values[i].isSeparator) {
separatorCount++
} else {
regularItemCount++
}
}
}
var separatorHeight = separatorCount * 12
var regularItemRows = Math.ceil(regularItemCount / 2)
var regularItemHeight = regularItemRows * 32
return Math.max(80, 35 + separatorHeight + regularItemHeight + 40)
}
QsMenuOpener {
id: opener
menu: root.menu
}
GridView {
id: gridView
anchors.fill: parent
anchors.margins: 20
cellWidth: width / 2
cellHeight: 32
interactive: false
flow: GridView.FlowLeftToRight
layoutDirection: Qt.LeftToRight
model: ScriptModel {
values: opener.children ? [...opener.children.values] : []
}
delegate: Item {
id: entry
required property var modelData
required property int index
width: gridView.cellWidth - 4
height: modelData.isSeparator ? 12 : 30
Rectangle {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
anchors.topMargin: 4
anchors.bottomMargin: 4
visible: modelData.isSeparator
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width * 0.8
height: 1
color: Qt.darker(Data.ThemeManager.accentColor, 1.5)
opacity: 0.6
}
}
Rectangle {
id: itemBackground
anchors.fill: parent
anchors.margins: 2
visible: !modelData.isSeparator
color: "transparent"
radius: 6
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 6
Image {
Layout.preferredWidth: 16
Layout.preferredHeight: 16
source: modelData?.icon ?? ""
visible: (modelData?.icon ?? "") !== ""
fillMode: Image.PreserveAspectFit
}
Text {
Layout.fillWidth: true
color: mouseArea.containsMouse ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
text: modelData?.text ?? ""
font.pixelSize: 11
font.family: "FiraCode Nerd Font"
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
maximumLineCount: 1
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
enabled: (modelData?.enabled ?? true) && root.visible && !modelData.isSeparator
onEntered: itemBackground.color = Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.15)
onExited: itemBackground.color = "transparent"
onClicked: {
modelData.triggered()
root.hide()
}
}
}
}
}
Item {
anchors.centerIn: gridView
visible: gridView.count === 0
Label {
anchors.centerIn: parent
text: "No tray items available"
color: Qt.darker(Data.ThemeManager.fgColor, 2)
font.pixelSize: 14
font.family: "FiraCode Nerd Font"
}
}
}

View file

@ -0,0 +1,234 @@
import Quickshell.Io
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import "root:/Data/" as Data
Rectangle {
id: root
required property var shell
property url avatarSource: Data.Settings.avatarSource
property string userName: "" // will be set by process output
property string userInfo: "" // will hold uptime string
property bool isActive: false
property bool isHovered: false // track hover state
radius: 20
width: 220
height: 80
color: {
if (isActive) {
return isHovered ?
Qt.lighter(Data.ThemeManager.accentColor, 1.1) :
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
} else {
return isHovered ?
Qt.lighter(Data.ThemeManager.accentColor, 1.2) :
Qt.lighter(Data.ThemeManager.bgColor, 1.15)
}
}
border.width: isActive ? 2 : 1
border.color: isActive ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.3)
Row {
anchors.fill: parent
anchors.margins: 14
spacing: 12
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: avatarCircle
width: 52
height: 52
radius: 20
clip: true
border.color: Data.ThemeManager.accentColor
border.width: 3
color: "transparent"
Image {
id: avatarImage
anchors.fill: parent
anchors.margins: 2
source: Data.Settings.avatarSource
fillMode: Image.PreserveAspectCrop
cache: false
visible: false // Hide the original image
asynchronous: true
sourceSize.width: 48 // Limit image resolution to save memory
sourceSize.height: 48
}
OpacityMask {
anchors.fill: avatarImage
source: avatarImage
maskSource: Rectangle {
width: avatarImage.width
height: avatarImage.height
radius: 18 // Proportionally smaller than parent (48/52 * 20 18)
visible: false
}
}
}
Column {
spacing: 4
anchors.verticalCenter: parent.verticalCenter
width: parent.width - avatarCircle.width - gifContainer.width - parent.spacing * 2
Text {
width: parent.width
text: root.userName === "" ? "Loading..." : root.userName
font.family: "Roboto"
font.pixelSize: 16
font.bold: true
color: isHovered || root.isActive ? Data.ThemeManager.bgColor : Data.ThemeManager.accentColor
elide: Text.ElideRight
maximumLineCount: 1
}
Text {
width: parent.width
text: root.userInfo === "" ? "Loading uptime..." : root.userInfo
font.family: "Roboto"
font.pixelSize: 11
font.bold: true
color: isHovered || root.isActive ? "#cccccc" : Qt.lighter(Data.ThemeManager.accentColor, 1.6)
elide: Text.ElideRight
maximumLineCount: 1
}
}
Rectangle {
id: gifContainer
width: 80
height: 80
radius: 12
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
AnimatedImage {
id: animatedImage
source: "root:/Assets/UserProfile.gif"
anchors.fill: parent
fillMode: Image.PreserveAspectFit
playing: true
cache: false
speed: 1.0
asynchronous: true
}
// Always enable layer effects for rounded corners
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: gifContainer.width
height: gifContainer.height
radius: gifContainer.radius
visible: false
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: root.isHovered = true
onExited: root.isHovered = false
}
Process {
id: usernameProcess
running: true // Always run to get username
command: ["sh", "-c", "whoami"]
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
const line = data.trim();
if (line.length > 0) {
root.userName = line.charAt(0).toUpperCase() + line.slice(1);
}
}
}
}
Process {
id: uptimeProcess
running: false
command: ["sh", "-c", "uptime"] // Use basic uptime command
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
const line = data.trim();
if (line.length > 0) {
// Parse traditional uptime output: " 10:30:00 up 1:23, 2 users, load average: 0.08, 0.02, 0.01"
const match = line.match(/up\s+(.+?),\s+\d+\s+user/);
if (match && match[1]) {
root.userInfo = "Up: " + match[1].trim();
} else {
// Fallback parsing
const upIndex = line.indexOf("up ");
if (upIndex !== -1) {
const afterUp = line.substring(upIndex + 3);
const commaIndex = afterUp.indexOf(",");
if (commaIndex !== -1) {
root.userInfo = "Up: " + afterUp.substring(0, commaIndex).trim();
} else {
root.userInfo = "Up: " + afterUp.trim();
}
} else {
root.userInfo = "Uptime unknown";
}
}
} else {
root.userInfo = "Uptime unknown";
}
}
}
stderr: SplitParser {
splitMarker: "\n"
onRead: (data) => {
console.log("Uptime error:", data);
root.userInfo = "Uptime error";
}
}
}
Timer {
id: uptimeTimer
interval: 300000 // Update every 5 minutes
running: true // Always run the uptime timer
repeat: true
onTriggered: {
uptimeProcess.running = false
uptimeProcess.running = true
}
}
Component.onCompleted: {
uptimeProcess.running = true // Start uptime process on component load
}
Component.onDestruction: {
if (usernameProcess.running) {
usernameProcess.running = false
}
if (uptimeProcess.running) {
uptimeProcess.running = false
}
if (uptimeTimer.running) {
uptimeTimer.running = false
}
}
}

View file

@ -0,0 +1,145 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import "root:/Data" as Data
Item {
id: root
property bool isVisible: false
signal visibilityChanged(bool visible)
// Use all space provided by parent
anchors.fill: parent
visible: isVisible
enabled: visible
clip: true
property bool containsMouse: wallpaperSelectorMouseArea.containsMouse || scrollView.containsMouse
property bool menuJustOpened: false
onContainsMouseChanged: {
if (containsMouse) {
hideTimer.stop()
} else if (!menuJustOpened && !isVisible) {
hideTimer.restart()
}
}
onVisibleChanged: {
if (visible) {
menuJustOpened = true
hideTimer.stop()
Qt.callLater(function() {
menuJustOpened = false
})
}
}
MouseArea {
id: wallpaperSelectorMouseArea
anchors.fill: parent
hoverEnabled: true
preventStealing: false
propagateComposedEvents: true
}
// Wallpaper grid - use all available space
ScrollView {
id: scrollView
anchors.fill: parent
clip: true
property bool containsMouse: gridMouseArea.containsMouse
MouseArea {
id: gridMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
}
GridView {
id: wallpaperGrid
anchors.fill: parent
cellWidth: parent.width / 2 - 8 // 2 columns with spacing for bigger previews
cellHeight: cellWidth * 0.6 // Aspect ratio for wallpapers
model: Data.WallpaperManager.wallpaperList
cacheBuffer: 0 // Disable cache buffer to save massive memory
leftMargin: 4
rightMargin: 4
topMargin: 4
bottomMargin: 4
delegate: Item {
width: wallpaperGrid.cellWidth - 8
height: wallpaperGrid.cellHeight - 8
Rectangle {
id: wallpaperItem
anchors.fill: parent
anchors.margins: 4
color: Qt.darker(Data.ThemeManager.bgColor, 1.2)
radius: 20
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
}
Image {
id: wallpaperImage
anchors.fill: parent
anchors.margins: 4
source: modelData
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false // Disable caching to save massive memory
sourceSize.width: Math.min(width, 150) // Further reduced from 200 to 150
sourceSize.height: Math.min(height, 90) // Further reduced from 120 to 90
// Only load when item is visible in viewport
visible: parent.parent.y >= wallpaperGrid.contentY - parent.parent.height &&
parent.parent.y <= wallpaperGrid.contentY + wallpaperGrid.height
// Disable layer effects to save memory
// layer.enabled: true
// layer.effect: OpacityMask {
// maskSource: Rectangle {
// width: wallpaperImage.width
// height: wallpaperImage.height
// radius: 18 // Slightly smaller than parent to account for margins
// }
// }
}
// Selected indicator
Rectangle {
visible: modelData === Data.WallpaperManager.currentWallpaper
anchors.fill: parent
radius: parent.radius
color: "transparent"
border.color: Data.ThemeManager.accentColor
border.width: 2
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: wallpaperItem.scale = 1.05
onExited: wallpaperItem.scale = 1.0
onClicked: {
Data.WallpaperManager.setWallpaper(modelData)
// Removed the close behavior - stays in wallpaper tab
}
}
}
}
}
}
Component.onCompleted: {
// Use lazy loading to only load wallpapers when this component is actually used
Data.WallpaperManager.ensureWallpapersLoaded()
}
}

View file

@ -0,0 +1,323 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
Rectangle {
id: root
required property var shell
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
property bool containsMouse: weatherMouseArea.containsMouse || (forecastPopup.visible && forecastPopup.containsMouse)
property bool menuJustOpened: false
signal entered()
signal exited()
onContainsMouseChanged: {
if (containsMouse) {
entered()
} else if (!menuJustOpened && !forecastPopup.visible) {
exited()
}
}
function getWeatherIcon(condition) {
if (!condition) return "light_mode"
const c = condition.toString()
const iconMap = {
"0": "light_mode",
"1": "light_mode",
"2": "cloud",
"3": "cloud",
"45": "foggy",
"48": "foggy",
"51": "water_drop",
"53": "water_drop",
"55": "water_drop",
"61": "water_drop",
"63": "water_drop",
"65": "water_drop",
"71": "ac_unit",
"73": "ac_unit",
"75": "ac_unit",
"80": "water_drop",
"81": "water_drop",
"82": "water_drop",
"95": "thunderstorm",
"96": "thunderstorm",
"99": "thunderstorm"
}
if (iconMap[c]) return iconMap[c]
const textMap = {
"clear sky": "light_mode",
"mainly clear": "light_mode",
"partly cloudy": "cloud",
"overcast": "cloud",
"fog": "foggy",
"drizzle": "water_drop",
"rain": "water_drop",
"snow": "ac_unit",
"thunderstorm": "thunderstorm"
}
const lower = condition.toLowerCase()
for (let key in textMap) {
if (lower.includes(key)) return textMap[key]
}
return "help"
}
MouseArea {
id: weatherMouseArea
anchors.fill: parent
hoverEnabled: true
onEntered: {
menuJustOpened = true
forecastPopup.open()
Qt.callLater(() => menuJustOpened = false)
}
onExited: {
if (!forecastPopup.containsMouse && !menuJustOpened) {
forecastPopup.close()
}
}
}
RowLayout {
id: weatherLayout
anchors.centerIn: parent
spacing: 8
ColumnLayout {
spacing: 2
Layout.alignment: Qt.AlignVCenter
Label {
text: {
if (shell.weatherLoading) return "refresh"
if (!shell.weatherData) return "help"
return root.getWeatherIcon(shell.weatherData.currentCondition)
}
font.pixelSize: 28
font.family: "Material Symbols Outlined"
color: Data.ThemeManager.accentColor
Layout.alignment: Qt.AlignHCenter
}
Label {
text: {
if (shell.weatherLoading) return "Loading..."
if (!shell.weatherData) return "No weather data"
return shell.weatherData.currentTemp
}
color: Data.ThemeManager.fgColor
font.pixelSize: 20
font.bold: true
Layout.alignment: Qt.AlignHCenter
}
}
}
Popup {
id: forecastPopup
y: parent.height + 28
x: Math.min(0, parent.width - width)
width: 300
height: 226
padding: 12
background: Rectangle {
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
border.width: 1
border.color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
property bool containsMouse: forecastMouseArea.containsMouse
onVisibleChanged: {
if (visible) {
entered()
} else if (!weatherMouseArea.containsMouse && !menuJustOpened) {
exited()
}
}
MouseArea {
id: forecastMouseArea
anchors.fill: parent
hoverEnabled: true
onExited: {
if (!weatherMouseArea.containsMouse && !menuJustOpened) {
forecastPopup.close()
}
}
}
ColumnLayout {
id: forecastColumn
anchors.fill: parent
anchors.margins: 10
spacing: 8
RowLayout {
Layout.fillWidth: true
spacing: 12
Label {
text: shell.weatherData ? root.getWeatherIcon(shell.weatherData.currentCondition) : ""
font.pixelSize: 48
font.family: "Material Symbols Outlined"
color: Data.ThemeManager.accentColor
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Label {
text: shell.weatherData ? shell.weatherData.currentCondition : ""
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
Layout.fillWidth: true
elide: Text.ElideRight
}
RowLayout {
spacing: 8
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
RowLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Label {
text: "thermostat"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: Data.ThemeManager.accentColor
}
Label {
text: shell.weatherData ? shell.weatherData.currentTemp : ""
color: Data.ThemeManager.fgColor
font.pixelSize: 12
}
}
Rectangle {
width: 1
height: 12
color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
RowLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Label {
text: "air"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: Data.ThemeManager.accentColor
}
Label {
text: {
if (!shell.weatherData || !shell.weatherData.details) return ""
const windInfo = shell.weatherData.details.find(d => d.startsWith("Wind:"))
return windInfo ? windInfo.split(": ")[1] : ""
}
color: Data.ThemeManager.fgColor
font.pixelSize: 12
}
}
Rectangle {
width: 1
height: 12
color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
RowLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Label {
text: "explore"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: Data.ThemeManager.accentColor
}
Label {
text: {
if (!shell.weatherData || !shell.weatherData.details) return ""
const dirInfo = shell.weatherData.details.find(d => d.startsWith("Direction:"))
return dirInfo ? dirInfo.split(": ")[1] : ""
}
color: Data.ThemeManager.fgColor
font.pixelSize: 12
}
}
Item {
Layout.fillWidth: true
}
}
}
}
Rectangle {
height: 1
Layout.fillWidth: true
color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
Label {
text: "3-Day Forecast"
color: Data.ThemeManager.accentColor
font.pixelSize: 12
font.bold: true
}
Row {
spacing: 8
Layout.fillWidth: true
Repeater {
model: shell.weatherData ? shell.weatherData.forecast : []
delegate: Column {
width: (parent.width - 16) / 3
spacing: 2
Label {
text: modelData.dayName
color: Data.ThemeManager.fgColor
font.pixelSize: 10
font.bold: true
anchors.horizontalCenter: parent.horizontalCenter
}
Label {
text: root.getWeatherIcon(modelData.condition)
font.pixelSize: 16
font.family: "Material Symbols Outlined"
color: Data.ThemeManager.accentColor
anchors.horizontalCenter: parent.horizontalCenter
}
Label {
text: modelData.minTemp + "° - " + modelData.maxTemp + "°"
color: Data.ThemeManager.fgColor
font.pixelSize: 10
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
}
}

View file

@ -0,0 +1,515 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Shapes
import Quickshell
import Quickshell.Io
import "root:/Data" as Data
// Clipboard history manager with cliphist integration
Item {
id: root
required property var shell
property string selectedWidget: "cliphist"
property bool isVisible: false
property real bgOpacity: 0.0
transformOrigin: Item.Center
function show() { showAnimation.start() }
function hide() { hideAnimation.start() }
function toggle() { isVisible ? hide() : show() }
// Smooth show/hide animations
ParallelAnimation {
id: showAnimation
PropertyAction { target: root; property: "isVisible"; value: true }
PropertyAnimation { target: root; property: "opacity"; from: 0.0; to: 1.0; duration: 200; easing.type: Easing.OutCubic }
PropertyAnimation { target: root; property: "scale"; from: 0.9; to: 1.0; duration: 200; easing.type: Easing.OutCubic }
}
ParallelAnimation {
id: hideAnimation
PropertyAnimation { target: root; property: "opacity"; to: 0.0; duration: 150; easing.type: Easing.InCubic }
PropertyAnimation { target: root; property: "scale"; to: 0.95; duration: 150; easing.type: Easing.InCubic }
PropertyAction { target: root; property: "isVisible"; value: false }
}
ColumnLayout {
id: contentColumn
anchors.fill: parent
spacing: 12
// Header
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: 30
Label {
text: "Clipboard History"
font.pixelSize: 16
font.weight: Font.Medium
color: Data.ThemeManager.fgColor
Layout.fillWidth: true
}
Button {
id: clearButton
text: "Clear"
implicitWidth: 60
implicitHeight: 25
background: Rectangle {
radius: 12
color: parent.down ? Qt.darker(Data.ThemeManager.accentColor, 1.2) :
parent.hovered ? Qt.lighter(Data.ThemeManager.accentColor, 1.1) :
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.8)
}
contentItem: Label {
text: parent.text
font.pixelSize: 11
color: Data.ThemeManager.fgColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: {
clearClipboardHistory()
clickScale.target = clearButton
clickScale.start()
}
}
}
// Scrollable clipboard history list
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ScrollView {
id: scrollView
anchors.fill: parent
clip: true
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
interactive: true
visible: cliphistList.contentHeight > cliphistList.height
contentItem: Rectangle {
implicitWidth: 6
radius: width / 2
color: parent.pressed ? Data.ThemeManager.accentColor
: parent.hovered ? Qt.lighter(Data.ThemeManager.accentColor, 1.2)
: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.7)
}
}
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ListView {
id: cliphistList
model: cliphistModel
spacing: 6
cacheBuffer: 50 // Memory optimization
reuseItems: true
boundsBehavior: Flickable.StopAtBounds
maximumFlickVelocity: 2500
flickDeceleration: 1500
// Smooth scrolling behavior
property real targetY: contentY
Behavior on targetY {
NumberAnimation {
duration: 200
easing.type: Easing.OutQuad
}
}
onTargetYChanged: {
if (!moving && !dragging) {
contentY = targetY
}
}
delegate: Rectangle {
width: cliphistList.width
height: Math.max(50, contentText.contentHeight + 20)
radius: 8
color: mouseArea.containsMouse ? Qt.darker(Data.ThemeManager.bgColor, 1.15) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: Data.ThemeManager.accentColor
border.width: 1
// View optimization - only render visible items
visible: y + height > cliphistList.contentY - height &&
y < cliphistList.contentY + cliphistList.height + height
RowLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
// Content type icon
Label {
text: model.type === "image" ? "🖼️" : model.type === "url" ? "🔗" : "📝"
font.pixelSize: 16
Layout.alignment: Qt.AlignTop
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 4
Label {
id: contentText
text: model.type === "image" ? "[Image Data]" :
(model.content.length > 100 ? model.content.substring(0, 100) + "..." : model.content)
font.pixelSize: 12
color: Data.ThemeManager.fgColor
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.fillHeight: true
elide: Text.ElideRight
maximumLineCount: 4
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
Label {
text: model.type === "image" ? "Image" : (model.content.length + " chars")
font.pixelSize: 10
color: Qt.darker(Data.ThemeManager.fgColor, 1.5)
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
copyToClipboard(model.id, model.type)
clickScale.target = parent
clickScale.start()
}
}
}
}
// Empty state message
Label {
anchors.centerIn: parent
text: "No clipboard history\nCopy something to get started"
font.pixelSize: 14
color: Qt.darker(Data.ThemeManager.fgColor, 1.5)
horizontalAlignment: Text.AlignHCenter
visible: cliphistList.count === 0
opacity: 0.7
}
}
}
}
}
// Click feedback animation
NumberAnimation {
id: clickScale
property Item target
properties: "scale"
from: 0.95
to: 1.0
duration: 150
easing.type: Easing.OutCubic
}
ListModel { id: cliphistModel }
property var currentEntries: []
// Main cliphist process for fetching clipboard history
Process {
id: cliphistProcess
command: ["cliphist", "list"]
running: false
property var tempEntries: []
onRunningChanged: {
if (running) {
tempEntries = []
} else {
// Process completed, apply smart diff update
updateModelIfChanged(tempEntries)
}
}
stdout: SplitParser {
onRead: data => {
try {
const line = data.toString().trim()
// Skip empty lines and error messages
if (line === "" || line.includes("ERROR") || line.includes("WARN") ||
line.includes("error:") || line.includes("warning:")) {
return
}
// Parse cliphist output format: ID + spaces + content
const match = line.match(/^(\d+)\s+(.+)$/)
if (match) {
const id = match[1]
const content = match[2]
cliphistProcess.tempEntries.push({
id: id,
content: content,
type: detectContentType(content)
})
} else {
console.log("Failed to parse line:", line)
}
} catch (e) {
console.error("Error parsing cliphist line:", e)
}
}
}
}
// Clear entire clipboard history
Process {
id: clearCliphistProcess
command: ["cliphist", "wipe"]
running: false
onRunningChanged: {
if (!running) {
cliphistModel.clear()
currentEntries = []
console.log("Clipboard history cleared")
}
}
stderr: SplitParser {
onRead: data => {
console.error("Clear clipboard error:", data.toString())
}
}
}
// Delete specific clipboard entry
Process {
id: deleteEntryProcess
property string entryId: ""
command: ["cliphist", "delete-query", entryId]
running: false
onRunningChanged: {
if (!running && entryId !== "") {
// Remove deleted entry from model
for (let i = 0; i < cliphistModel.count; i++) {
if (cliphistModel.get(i).id === entryId) {
cliphistModel.remove(i)
currentEntries = currentEntries.filter(entry => entry.id !== entryId)
break
}
}
console.log("Deleted entry:", entryId)
entryId = ""
}
}
stderr: SplitParser {
onRead: data => {
console.error("Delete entry error:", data.toString())
}
}
}
// Copy plain text to clipboard
Process {
id: copyTextProcess
property string textToCopy: ""
command: ["wl-copy", textToCopy]
running: false
stderr: SplitParser {
onRead: data => {
console.error("wl-copy error:", data.toString())
}
}
}
// Copy from clipboard history
Process {
id: copyHistoryProcess
property string entryId: ""
command: ["sh", "-c", "printf '%s' '" + entryId + "' | cliphist decode | wl-copy"]
running: false
stderr: SplitParser {
onRead: data => {
console.error("Copy history error:", data.toString())
}
}
}
// Periodic refresh timer (disabled by default)
Timer {
id: refreshTimer
interval: 30000
running: false // Only enable when needed
repeat: true
onTriggered: {
if (!cliphistProcess.running && root.isVisible) {
refreshClipboardHistory()
}
}
}
// Component initialization
Component.onCompleted: {
refreshClipboardHistory()
}
onIsVisibleChanged: {
if (isVisible && cliphistModel.count === 0) {
refreshClipboardHistory()
}
}
// Smart model update - only changes when content differs
function updateModelIfChanged(newEntries) {
// Quick length check
if (newEntries.length !== currentEntries.length) {
updateModel(newEntries)
return
}
// Compare content for changes
let hasChanges = false
for (let i = 0; i < newEntries.length; i++) {
if (i >= currentEntries.length ||
newEntries[i].id !== currentEntries[i].id ||
newEntries[i].content !== currentEntries[i].content) {
hasChanges = true
break
}
}
if (hasChanges) {
updateModel(newEntries)
}
}
// Efficient model update with scroll position preservation
function updateModel(newEntries) {
const scrollPos = cliphistList.contentY
// Remove obsolete items
for (let i = cliphistModel.count - 1; i >= 0; i--) {
const modelItem = cliphistModel.get(i)
const found = newEntries.some(entry => entry.id === modelItem.id)
if (!found) {
cliphistModel.remove(i)
}
}
// Add or update items
for (let i = 0; i < newEntries.length; i++) {
const newEntry = newEntries[i]
let found = false
// Check if item exists and update position
for (let j = 0; j < cliphistModel.count; j++) {
const modelItem = cliphistModel.get(j)
if (modelItem.id === newEntry.id) {
if (modelItem.content !== newEntry.content) {
cliphistModel.set(j, newEntry)
}
if (j !== i && i < cliphistModel.count) {
cliphistModel.move(j, i, 1)
}
found = true
break
}
}
// Add new item
if (!found) {
if (i < cliphistModel.count) {
cliphistModel.insert(i, newEntry)
} else {
cliphistModel.append(newEntry)
}
}
}
// Restore scroll position
cliphistList.contentY = scrollPos
currentEntries = newEntries.slice()
}
// Content type detection based on patterns
function detectContentType(content) {
// Binary/image data detection
if (content.includes('\x00') || content.startsWith('\x89PNG') || content.startsWith('\xFF\xD8\xFF')) {
return "image"
}
if (content.includes('[[ binary data ') || content.includes('<selection>')) {
return "image"
}
// URL detection
if (/^https?:\/\/\S+$/.test(content.trim())) return "url"
// Code detection
if (content.includes('\n') && (content.includes('{') || content.includes('function') || content.includes('=>'))) return "code"
// Command detection
if (content.startsWith('sudo ') || content.startsWith('pacman ') || content.startsWith('apt ')) return "command"
return "text"
}
function formatTimestamp(timestamp) {
const now = new Date()
const entryDate = new Date(parseInt(timestamp))
const diff = (now - entryDate) / 1000
if (diff < 60) return "Just now"
if (diff < 3600) return Math.floor(diff / 60) + " min ago"
if (diff < 86400) return Math.floor(diff / 3600) + " hour" + (Math.floor(diff / 3600) === 1 ? "" : "s") + " ago"
return Qt.formatDateTime(entryDate, "MMM d h:mm AP")
}
function clearClipboardHistory() {
clearCliphistProcess.running = true
}
function deleteClipboardEntry(entryId) {
deleteEntryProcess.entryId = entryId
deleteEntryProcess.running = true
}
function refreshClipboardHistory() {
cliphistProcess.running = true
}
// Copy handler - chooses appropriate method based on content type
function copyToClipboard(entryIdOrText, contentType) {
if (contentType === "image" || typeof entryIdOrText === "string" && entryIdOrText.match(/^\d+$/)) {
// Use cliphist decode for binary data and numbered entries
copyHistoryProcess.entryId = entryIdOrText
copyHistoryProcess.running = true
} else {
// Use wl-copy for plain text
copyTextProcess.textToCopy = entryIdOrText
copyTextProcess.running = true
}
}
// Clean up all processes on destruction
Component.onDestruction: {
cliphistProcess.running = false
clearCliphistProcess.running = false
deleteEntryProcess.running = false
copyTextProcess.running = false
copyHistoryProcess.running = false
}
}

View file

@ -0,0 +1,161 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import "root:/Data/" as Data
// Custom system tray menu
Rectangle {
id: trayMenu
implicitWidth: 360
implicitHeight: Math.max(40, listView.contentHeight + 12 + 16)
clip: true
color: Data.ThemeManager.bgColor
border.color: Data.ThemeManager.accentColor
border.width: 3
radius: 20
visible: false
enabled: visible
property QsMenuHandle menu
property point triggerPoint: Qt.point(0, 0)
property Item originalParent
// Menu opener handles native menu integration
QsMenuOpener {
id: opener
menu: trayMenu.menu
}
// Full-screen overlay to capture outside clicks
Rectangle {
id: overlay
x: -trayMenu.x
y: -trayMenu.y
width: Screen.width
height: Screen.height
color: "transparent"
visible: trayMenu.visible
z: -1
MouseArea {
anchors.fill: parent
enabled: trayMenu.visible
acceptedButtons: Qt.AllButtons
onPressed: {
trayMenu.hide()
}
}
}
// Flatten hierarchical menu structure into single list
function flattenMenuItems(menuHandle) {
var result = [];
if (!menuHandle || !menuHandle.children) {
return result;
}
var childrenArray = [];
for (var i = 0; i < menuHandle.children.length; i++) {
childrenArray.push(menuHandle.children[i]);
}
for (var i = 0; i < childrenArray.length; i++) {
var item = childrenArray[i];
if (item.isSeparator) {
result.push(item);
} else if (item.menu) {
// Add parent item and its submenu items
result.push(item);
var submenuItems = flattenMenuItems(item.menu);
result = result.concat(submenuItems);
} else {
result.push(item);
}
}
return result;
}
// Menu item list
ListView {
id: listView
anchors.fill: parent
anchors.margins: 6
anchors.topMargin: 3
anchors.bottomMargin: 9
model: ScriptModel {
values: flattenMenuItems(opener.menu)
}
interactive: false
delegate: Rectangle {
id: entry
required property var modelData
width: listView.width - 12
height: modelData.isSeparator ? 10 : 28
color: modelData.isSeparator ? Data.ThemeManager.bgColor : (mouseArea.containsMouse ? Data.ThemeManager.highlightBg : "transparent")
radius: modelData.isSeparator ? 0 : 4
// Separator line rendering
Item {
anchors.fill: parent
visible: modelData.isSeparator
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: parent.width * 0.85
height: 1
color: Data.ThemeManager.accentColor
opacity: 0.3
}
}
// Menu item content (text and icon)
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 6
visible: !modelData.isSeparator
Text {
Layout.fillWidth: true
color: (modelData?.enabled ?? true) ? Data.ThemeManager.fgColor : Qt.darker(Data.ThemeManager.fgColor, 1.8)
text: modelData?.text ?? ""
font.pixelSize: 12
font.family: "FiraCode Nerd Font"
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
maximumLineCount: 1
}
Image {
Layout.preferredWidth: 14
Layout.preferredHeight: 14
source: modelData?.icon ?? ""
visible: (modelData?.icon ?? "") !== ""
fillMode: Image.PreserveAspectFit
}
}
// Click handling
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
enabled: (modelData?.enabled ?? true) && trayMenu.visible && !modelData.isSeparator
onClicked: {
if (modelData) {
modelData.triggered()
trayMenu.hide()
}
}
}
}
}
}

View file

@ -0,0 +1,442 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Shapes
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import "root:/Data" as Data
import "root:/Core" as Core
// Niri workspace indicator
Rectangle {
id: root
property ListModel workspaces: ListModel {}
property int currentWorkspace: -1
property bool isDestroying: false
// Signal for workspace change bursts
signal workspaceChanged(int workspaceId, color accentColor)
// MASTER ANIMATION CONTROLLER - drives Desktop overlay burst effect
property real masterProgress: 0.0
property bool effectsActive: false
property color effectColor: Data.ThemeManager.accent
// Single master animation that controls Desktop overlay burst
function triggerUnifiedWave() {
effectColor = Data.ThemeManager.accent
masterAnimation.restart()
}
SequentialAnimation {
id: masterAnimation
PropertyAction {
target: root
property: "effectsActive"
value: true
}
NumberAnimation {
target: root
property: "masterProgress"
from: 0.0
to: 1.0
duration: 1000
easing.type: Easing.OutQuint
}
PropertyAction {
target: root
property: "effectsActive"
value: false
}
PropertyAction {
target: root
property: "masterProgress"
value: 0.0
}
}
color: Data.ThemeManager.bgColor
width: 32
height: workspaceColumn.implicitHeight + 24
// Smooth height animation
Behavior on height {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
// Right-side rounded corners
topRightRadius: width / 2
bottomRightRadius: width / 2
topLeftRadius: 0
bottomLeftRadius: 0
// Wave effects overlay - unified animation system (DISABLED - using Desktop overlay)
Item {
id: waveEffects
anchors.fill: parent
visible: false // Disabled in favor of unified overlay
z: 2
}
// Niri event stream listener
Process {
id: niriProcess
command: ["niri", "msg", "event-stream"]
running: true
stdout: SplitParser {
onRead: data => {
const lines = data.split('\n');
for (const line of lines) {
if (line.trim()) {
parseNiriEvent(line.trim());
}
}
}
}
onExited: {
// Auto-restart on failure to maintain workspace sync (but not during destruction)
if (exitCode !== 0 && !root.isDestroying) {
Qt.callLater(() => running = true);
}
}
}
// Parse Niri event stream messages
function parseNiriEvent(line) {
try {
// Handle workspace focus changes
if (line.startsWith("Workspace focused: ")) {
const workspaceId = parseInt(line.replace("Workspace focused: ", ""));
if (!isNaN(workspaceId)) {
const previousWorkspace = root.currentWorkspace;
root.currentWorkspace = workspaceId;
updateWorkspaceFocus(workspaceId);
// Trigger burst effect if workspace actually changed
if (previousWorkspace !== workspaceId && previousWorkspace !== -1) {
root.triggerUnifiedWave();
root.workspaceChanged(workspaceId, Data.ThemeManager.accent);
}
}
}
// Handle workspace list updates
else if (line.startsWith("Workspaces changed: ")) {
const workspaceData = line.replace("Workspaces changed: ", "");
parseWorkspaceList(workspaceData);
}
} catch (e) {
console.log("Error parsing niri event:", e);
}
}
// Update workspace focus states
function updateWorkspaceFocus(focusedWorkspaceId) {
for (let i = 0; i < root.workspaces.count; i++) {
const workspace = root.workspaces.get(i);
const wasFocused = workspace.isFocused;
const isFocused = workspace.id === focusedWorkspaceId;
const isActive = workspace.id === focusedWorkspaceId;
// Only update changed properties to trigger animations
if (wasFocused !== isFocused) {
root.workspaces.setProperty(i, "isFocused", isFocused);
root.workspaces.setProperty(i, "isActive", isActive);
}
}
}
// Parse workspace data from Niri's Rust-style output format
function parseWorkspaceList(data) {
try {
const workspaceMatches = data.match(/Workspace \{[^}]+\}/g);
if (!workspaceMatches) {
return;
}
const newWorkspaces = [];
for (const match of workspaceMatches) {
const idMatch = match.match(/id: (\d+)/);
const idxMatch = match.match(/idx: (\d+)/);
const nameMatch = match.match(/name: Some\("([^"]+)"\)|name: None/);
const outputMatch = match.match(/output: Some\("([^"]+)"\)/);
const isActiveMatch = match.match(/is_active: (true|false)/);
const isFocusedMatch = match.match(/is_focused: (true|false)/);
const isUrgentMatch = match.match(/is_urgent: (true|false)/);
if (idMatch && idxMatch && outputMatch) {
const workspace = {
id: parseInt(idMatch[1]),
idx: parseInt(idxMatch[1]),
name: nameMatch && nameMatch[1] ? nameMatch[1] : "",
output: outputMatch[1],
isActive: isActiveMatch ? isActiveMatch[1] === "true" : false,
isFocused: isFocusedMatch ? isFocusedMatch[1] === "true" : false,
isUrgent: isUrgentMatch ? isUrgentMatch[1] === "true" : false
};
newWorkspaces.push(workspace);
if (workspace.isFocused) {
root.currentWorkspace = workspace.id;
}
}
}
// Sort by index and update model
newWorkspaces.sort((a, b) => a.idx - b.idx);
root.workspaces.clear();
root.workspaces.append(newWorkspaces);
} catch (e) {
console.log("Error parsing workspace list:", e);
}
}
// Vertical workspace indicator pills
Column {
id: workspaceColumn
anchors.centerIn: parent
spacing: 6
Repeater {
model: root.workspaces
Rectangle {
id: workspacePill
// Dynamic sizing based on focus state
width: model.isFocused ? 18 : 16
height: model.isFocused ? 36 : 22
radius: width / 2
scale: model.isFocused ? 1.0 : 0.9
// Material Design 3 inspired colors
color: {
if (model.isFocused) {
return Data.ThemeManager.accent;
}
if (model.isActive) {
return Qt.rgba(Data.ThemeManager.accent.r, Data.ThemeManager.accent.g, Data.ThemeManager.accent.b, 0.5);
}
if (model.isUrgent) {
return Data.ThemeManager.error;
}
return Qt.rgba(Data.ThemeManager.primaryText.r, Data.ThemeManager.primaryText.g, Data.ThemeManager.primaryText.b, 0.4);
}
// Workspace pill burst overlay (DISABLED - using unified overlay)
Rectangle {
id: pillBurst
anchors.centerIn: parent
width: parent.width + 8
height: parent.height + 8
radius: width / 2
color: Data.ThemeManager.accent
opacity: 0 // Disabled in favor of unified overlay
visible: false
z: -1
}
// Subtle pulse for inactive pills during workspace changes
Rectangle {
id: inactivePillPulse
anchors.fill: parent
radius: parent.radius
color: Data.ThemeManager.accent
opacity: {
// Only pulse inactive pills during effects
if (model.isFocused || !root.effectsActive) return 0
// More subtle pulse that peaks mid-animation
if (root.masterProgress < 0.3) {
return (root.masterProgress / 0.3) * 0.15
} else if (root.masterProgress < 0.7) {
return 0.15
} else {
return 0.15 * (1.0 - (root.masterProgress - 0.7) / 0.3)
}
}
z: -0.5 // Behind the pill content but visible
}
// Enhanced corner shadows for burst effect (DISABLED - using unified overlay)
Rectangle {
id: cornerBurst
anchors.centerIn: parent
width: parent.width + 4
height: parent.height + 4
radius: width / 2
color: "transparent"
border.color: Data.ThemeManager.accent
border.width: 0 // Disabled
opacity: 0 // Disabled in favor of unified overlay
visible: false
z: 1
}
// Elevation shadow
Rectangle {
anchors.fill: parent
anchors.topMargin: model.isFocused ? 1 : 0
anchors.leftMargin: model.isFocused ? 0.5 : 0
anchors.rightMargin: model.isFocused ? -0.5 : 0
anchors.bottomMargin: model.isFocused ? -1 : 0
radius: parent.radius
color: Qt.rgba(0, 0, 0, model.isFocused ? 0.15 : 0)
z: -1
visible: model.isFocused
Behavior on color { ColorAnimation { duration: 200 } }
}
// Smooth Material Design transitions
Behavior on width {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
Behavior on height {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
Behavior on scale {
NumberAnimation {
duration: 250
easing.type: Easing.OutCubic
}
}
Behavior on color {
ColorAnimation {
duration: 200
}
}
// Workspace number text
Text {
anchors.centerIn: parent
text: model.idx.toString()
color: model.isFocused ? Data.ThemeManager.background : Data.ThemeManager.primaryText
font.pixelSize: model.isFocused ? 10 : 8
font.bold: model.isFocused
font.family: "Roboto, sans-serif"
visible: model.isFocused || model.isActive
Behavior on font.pixelSize { NumberAnimation { duration: 200 } }
Behavior on color { ColorAnimation { duration: 200 } }
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
// Switch workspace via Niri command
switchProcess.command = ["niri", "msg", "action", "focus-workspace", model.idx.toString()];
switchProcess.running = true;
}
// Hover feedback
onEntered: {
if (!model.isFocused) {
workspacePill.color = Qt.rgba(Data.ThemeManager.primaryText.r, Data.ThemeManager.primaryText.g, Data.ThemeManager.primaryText.b, 0.6);
}
}
onExited: {
// Reset to normal color
if (!model.isFocused) {
if (model.isActive) {
workspacePill.color = Qt.rgba(Data.ThemeManager.accent.r, Data.ThemeManager.accent.g, Data.ThemeManager.accent.b, 0.5);
} else if (model.isUrgent) {
workspacePill.color = Data.ThemeManager.error;
} else {
workspacePill.color = Qt.rgba(Data.ThemeManager.primaryText.r, Data.ThemeManager.primaryText.g, Data.ThemeManager.primaryText.b, 0.4);
}
}
}
}
}
}
}
// Workspace switching command process
Process {
id: switchProcess
running: false
onExited: {
running = false
if (exitCode !== 0) {
console.log("Failed to switch workspace:", exitCode);
}
}
}
// Border integration corners
Core.Corners {
id: topLeftCorner
position: "topleft"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: -41
offsetY: -25
}
// Top-left corner wave overlay (DISABLED - using unified overlay)
Shape {
id: topLeftWave
width: topLeftCorner.width
height: topLeftCorner.height
x: topLeftCorner.x
y: topLeftCorner.y
visible: false // Disabled in favor of unified overlay
preferredRendererType: Shape.CurveRenderer
layer.enabled: true
layer.samples: 4
}
Core.Corners {
id: bottomLeftCorner
position: "bottomleft"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: -41
offsetY: 78
}
// Bottom-left corner wave overlay (DISABLED - using unified overlay)
Shape {
id: bottomLeftWave
width: bottomLeftCorner.width
height: bottomLeftCorner.height
x: bottomLeftCorner.x
y: bottomLeftCorner.y
visible: false // Disabled in favor of unified overlay
preferredRendererType: Shape.CurveRenderer
layer.enabled: true
layer.samples: 4
}
// Clean up processes on destruction
Component.onDestruction: {
root.isDestroying = true
if (niriProcess.running) {
niriProcess.running = false
}
if (switchProcess.running) {
switchProcess.running = false
}
}
}

View file

@ -0,0 +1,228 @@
import QtQuick
import Quickshell
import Quickshell.Io
import QtQuick.Layouts
import QtQuick.Shapes
import "root:/Data/" as Data
import "root:/Core" as Core
Item {
id: osd
property var shell
QtObject {
id: modeEnum
readonly property int volume: 0
readonly property int brightness: 1
}
property int mode: -1
property int lastVolume: -1
property int lastBrightness: -1
width: osdBackground.width
height: osdBackground.height
visible: false
Timer {
id: hideTimer
interval: 2500
onTriggered: hideOsd()
}
FileView {
id: brightnessFile
path: "/tmp/brightness_osd_level"
watchChanges: true
blockLoading: true
onLoaded: updateBrightness()
onFileChanged: {
brightnessFile.reload();
updateBrightness();
}
function updateBrightness() {
const val = parseInt(brightnessFile.text());
if (!isNaN(val) && val !== lastBrightness) {
lastBrightness = val;
mode = modeEnum.brightness;
showOsd();
}
}
}
Connections {
target: shell
function onVolumeChanged() {
if (shell.volume !== lastVolume && lastVolume !== -1) {
lastVolume = shell.volume;
mode = modeEnum.volume;
showOsd();
}
lastVolume = shell.volume;
}
}
Component.onCompleted: {
if (shell?.volume !== undefined)
lastVolume = shell.volume;
}
function showOsd() {
if (!osd.visible) {
osd.visible = true;
slideInAnimation.start();
}
hideTimer.restart();
}
function hideOsd() {
slideOutAnimation.start();
}
NumberAnimation {
id: slideInAnimation
target: osdBackground
property: "x"
from: osd.width
to: 0
duration: 300
easing.type: Easing.OutCubic
}
NumberAnimation {
id: slideOutAnimation
target: osdBackground
property: "x"
from: 0
to: osd.width
duration: 250
easing.type: Easing.InCubic
onFinished: {
osd.visible = false;
osdBackground.x = 0;
}
}
Rectangle {
id: osdBackground
width: 45
height: 250
color: Data.ThemeManager.bgColor
topLeftRadius: 20
bottomLeftRadius: 20
Column {
anchors.fill: parent
anchors.margins: 16
spacing: 12
Text {
id: osdIcon
font.family: "Roboto"
font.pixelSize: 16
color: Data.ThemeManager.fgColor
text: {
if (mode === modeEnum.volume) {
if (!shell || shell.volume === undefined)
return "󰝟";
const vol = shell.volume;
return vol === 0 ? "󰝟" : vol < 33 ? "󰕿" : vol < 66 ? "󰖀" : "󰕾";
} else if (mode === modeEnum.brightness) {
const b = lastBrightness;
return b < 0 ? "󰃞" : b < 33 ? "󰃟" : b < 66 ? "󰃠" : "󰃝";
}
return "";
}
anchors.horizontalCenter: parent.horizontalCenter
Behavior on text {
SequentialAnimation {
PropertyAnimation {
target: osdIcon
property: "scale"
to: 1.2
duration: 100
}
PropertyAnimation {
target: osdIcon
property: "scale"
to: 1.0
duration: 100
}
}
}
}
Rectangle {
width: 10
height: parent.height - osdIcon.height - osdLabel.height - 36
radius: 5
color: Qt.darker(Data.ThemeManager.accentColor, 1.5)
border.color: Qt.darker(Data.ThemeManager.accentColor, 2.0)
border.width: 1
anchors.horizontalCenter: parent.horizontalCenter
Rectangle {
id: fillBar
width: parent.width - 2
radius: parent.radius - 1
x: 1
color: Data.ThemeManager.accentColor
anchors.bottom: parent.bottom
anchors.bottomMargin: 1
height: {
const val = mode === modeEnum.volume ? shell?.volume : lastBrightness;
const maxHeight = parent.height - 2;
return maxHeight * Math.max(0, Math.min(1, val / 100));
}
Behavior on height {
NumberAnimation {
duration: 250
easing.type: Easing.OutCubic
}
}
}
}
Text {
id: osdLabel
text: {
const val = mode === modeEnum.volume ? shell?.volume : lastBrightness;
return val >= 0 ? val + "%" : "0%";
}
font.pixelSize: 10
font.weight: Font.Bold
color: Data.ThemeManager.fgColor
anchors.horizontalCenter: parent.horizontalCenter
Behavior on text {
PropertyAnimation {
target: osdLabel
property: "opacity"
from: 0.7
to: 1.0
duration: 150
}
}
}
}
}
Core.Corners {
position: "bottomright"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: 39 + osdBackground.x
offsetY: 78
}
Core.Corners {
position: "topright"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: 39 + osdBackground.x
offsetY: -26
}
}

View file

@ -0,0 +1,181 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Services.SystemTray
import "root:/Data" as Data
// System tray with optimized icon caching
Row {
property var bar
property var shell
property var trayMenu
spacing: 8
Layout.alignment: Qt.AlignVCenter
property bool containsMouse: false
property var systemTray: SystemTray
// Custom icon cache for memory optimization
property var iconCache: ({})
property var iconCacheCount: ({})
// Cache cleanup to prevent memory leaks
Timer {
interval: 120000
repeat: true
running: systemTray.items.length > 0
onTriggered: {
// Decrement counters and remove unused icons
for (let icon in iconCacheCount) {
iconCacheCount[icon]--
if (iconCacheCount[icon] <= 0) {
delete iconCache[icon]
delete iconCacheCount[icon]
}
}
// Enforce maximum cache size
const maxCacheSize = 10;
const cacheKeys = Object.keys(iconCache);
if (cacheKeys.length > maxCacheSize) {
const toRemove = cacheKeys.slice(0, cacheKeys.length - maxCacheSize);
toRemove.forEach(key => {
delete iconCache[key];
delete iconCacheCount[key];
});
}
}
}
Repeater {
model: systemTray.items
delegate: Item {
width: 24
height: 24
property bool isHovered: trayMouseArea.containsMouse
onIsHoveredChanged: updateParentHoverState()
Component.onCompleted: updateParentHoverState()
function updateParentHoverState() {
let anyHovered = false
for (let i = 0; i < parent.children.length; i++) {
if (parent.children[i].isHovered) {
anyHovered = true
break
}
}
parent.containsMouse = anyHovered
}
// Hover animations
scale: isHovered ? 1.15 : 1.0
Behavior on scale {
enabled: isHovered
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
rotation: isHovered ? 5 : 0
Behavior on rotation {
enabled: isHovered
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
Image {
id: trayIcon
anchors.centerIn: parent
width: 18
height: 18
sourceSize.width: 18
sourceSize.height: 18
smooth: false // Memory savings
asynchronous: true
cache: false // Use custom cache instead
source: {
let icon = modelData?.icon || "";
if (!icon) return "";
// Return cached icon if available
if (iconCache[icon]) {
iconCacheCount[icon] = 2
return iconCache[icon];
}
// Process icon path
let finalPath = icon;
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
const fileName = name.substring(name.lastIndexOf("/") + 1);
finalPath = `file://${path}/${fileName}`;
}
// Cache the processed path
iconCache[icon] = finalPath;
iconCacheCount[icon] = 2;
return finalPath;
}
opacity: status === Image.Ready ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
}
Component.onDestruction: {
let icon = modelData?.icon || "";
if (icon) {
delete iconCache[icon];
delete iconCacheCount[icon];
}
}
MouseArea {
id: trayMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: (mouse) => {
if (!modelData) return;
if (mouse.button === Qt.LeftButton) {
if (trayMenu && trayMenu.visible) {
trayMenu.hide()
}
if (!modelData.onlyMenu) {
modelData.activate()
}
} else if (mouse.button === Qt.MiddleButton) {
if (trayMenu && trayMenu.visible) {
trayMenu.hide()
}
modelData.secondaryActivate && modelData.secondaryActivate()
} else if (mouse.button === Qt.RightButton) {
if (trayMenu && trayMenu.visible) {
trayMenu.hide()
return
}
// Show context menu if available
if (modelData.hasMenu && modelData.menu && trayMenu) {
trayMenu.menu = modelData.menu
const iconCenter = Qt.point(width / 2, height)
const iconPos = mapToItem(trayMenu.parent, 0, 0)
const menuX = iconPos.x - (trayMenu.width / 2) + (width / 2)
const menuY = iconPos.y + height + 15
trayMenu.show(Qt.point(menuX, menuY), trayMenu.parent)
}
}
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more