add crypto
This commit is contained in:
parent
90cbe489f6
commit
af6a3bce3e
120 changed files with 24616 additions and 462 deletions
18
.sops.yaml
Normal file
18
.sops.yaml
Normal 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
569
flake.lock
generated
File diff suppressed because it is too large
Load diff
15
flake.nix
15
flake.nix
|
|
@ -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; [
|
||||
|
|
|
|||
|
|
@ -44,8 +44,6 @@
|
|||
catppuccin.fzf.enable = true;
|
||||
|
||||
home.packages = with pkgs; [
|
||||
devenv
|
||||
|
||||
(discord.override {
|
||||
withOpenASAR = true;
|
||||
withVencord = true;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
return {
|
||||
"chomosuke/typst-preview.nvim",
|
||||
lazy = false, -- or ft = 'typst'
|
||||
version = "1.*",
|
||||
opts = {}, -- lazy.nvim will implicitly calls `setup {}`
|
||||
}
|
||||
261
modules/home/apps/web/floorp/default.nix
Normal file
261
modules/home/apps/web/floorp/default.nix
Normal 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}")
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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")
|
||||
];
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
BIN
modules/home/services/quickshell/qml/Assets/UserProfile.gif
Normal file
BIN
modules/home/services/quickshell/qml/Assets/UserProfile.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
80
modules/home/services/quickshell/qml/Core/Corners.qml
Normal file
80
modules/home/services/quickshell/qml/Core/Corners.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
48
modules/home/services/quickshell/qml/Core/LoaderManager.qml
Normal file
48
modules/home/services/quickshell/qml/Core/LoaderManager.qml
Normal 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]
|
||||
}
|
||||
}
|
||||
175
modules/home/services/quickshell/qml/Core/ProcessManager.qml
Normal file
175
modules/home/services/quickshell/qml/Core/ProcessManager.qml
Normal 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
|
||||
}
|
||||
}
|
||||
265
modules/home/services/quickshell/qml/Core/Version.qml
Normal file
265
modules/home/services/quickshell/qml/Core/Version.qml
Normal 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
|
||||
}
|
||||
}
|
||||
47
modules/home/services/quickshell/qml/Core/Wallpaper.qml
Normal file
47
modules/home/services/quickshell/qml/Core/Wallpaper.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
38
modules/home/services/quickshell/qml/Data/MatugenManager.qml
Normal file
38
modules/home/services/quickshell/qml/Data/MatugenManager.qml
Normal 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
|
||||
}
|
||||
}
|
||||
333
modules/home/services/quickshell/qml/Data/Settings.qml
Normal file
333
modules/home/services/quickshell/qml/Data/Settings.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
243
modules/home/services/quickshell/qml/Data/ThemeManager.qml
Normal file
243
modules/home/services/quickshell/qml/Data/ThemeManager.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
46
modules/home/services/quickshell/qml/Data/Themes/Dracula.qml
Normal file
46
modules/home/services/quickshell/qml/Data/Themes/Dracula.qml
Normal 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
|
||||
})
|
||||
}
|
||||
46
modules/home/services/quickshell/qml/Data/Themes/Gruvbox.qml
Normal file
46
modules/home/services/quickshell/qml/Data/Themes/Gruvbox.qml
Normal 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
|
||||
})
|
||||
}
|
||||
143
modules/home/services/quickshell/qml/Data/Themes/Matugen.qml
Normal file
143
modules/home/services/quickshell/qml/Data/Themes/Matugen.qml
Normal 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
170
modules/home/services/quickshell/qml/Data/WallpaperManager.qml
Normal file
170
modules/home/services/quickshell/qml/Data/WallpaperManager.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
9
modules/home/services/quickshell/qml/Data/settings.json
Normal file
9
modules/home/services/quickshell/qml/Data/settings.json
Normal 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"
|
||||
}
|
||||
35
modules/home/services/quickshell/qml/Layout/Bar.qml
Normal file
35
modules/home/services/quickshell/qml/Layout/Bar.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
575
modules/home/services/quickshell/qml/Layout/Border.qml
Normal file
575
modules/home/services/quickshell/qml/Layout/Border.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
318
modules/home/services/quickshell/qml/Layout/Desktop.qml
Normal file
318
modules/home/services/quickshell/qml/Layout/Desktop.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
140
modules/home/services/quickshell/qml/Services/MatugenService.qml
Normal file
140
modules/home/services/quickshell/qml/Services/MatugenService.qml
Normal 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
269
modules/home/services/quickshell/qml/Services/WeatherService.qml
Normal file
269
modules/home/services/quickshell/qml/Services/WeatherService.qml
Normal 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}¤t=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 = "";
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
64
modules/home/services/quickshell/qml/Widgets/Clock.qml
Normal file
64
modules/home/services/quickshell/qml/Widgets/Clock.qml
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
143
modules/home/services/quickshell/qml/Widgets/Panel/TopPanel.qml
Normal file
143
modules/home/services/quickshell/qml/Widgets/Panel/TopPanel.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
515
modules/home/services/quickshell/qml/Widgets/System/Cliphist.qml
Normal file
515
modules/home/services/quickshell/qml/Widgets/System/Cliphist.qml
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
228
modules/home/services/quickshell/qml/Widgets/System/OSD.qml
Normal file
228
modules/home/services/quickshell/qml/Widgets/System/OSD.qml
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue