mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Compare commits
253 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 349ef85648 | |||
| b38cf8ef66 | |||
| 23c83a49c3 | |||
| 1926008315 | |||
| deb75f5bab | |||
| 53baf1c86b | |||
| 8173919692 | |||
| ece8705e5d | |||
| 346d29d94a | |||
| a3f604efc3 | |||
| 0e8a920ee2 | |||
| e98e034a68 | |||
| 1f3cafb1b9 | |||
| 316cd3114a | |||
| 4c951cf380 | |||
| 0f888fd734 | |||
| 0690ac4996 | |||
| 3809f290ed | |||
| b1094bbfa0 | |||
| 644e24f409 | |||
| 6f2d7516f0 | |||
| 8dad25f79c | |||
| 36489491e4 | |||
| 846730361d | |||
| 428f3627b6 | |||
| 68b328c982 | |||
| 4dac2ffe88 | |||
| f3535f22ba | |||
| deca5e1235 | |||
| 8da903bb61 | |||
| b58f6f0a1b | |||
| 946996917d | |||
| b03b4b0f13 | |||
| 73f76e2275 | |||
| 80442e2839 | |||
| a8a1b0a422 | |||
| 346e27830a | |||
| ef616efcca | |||
| 8c1153192d | |||
| c46a84d794 | |||
| 46d3465b50 | |||
| 7bd278d428 | |||
| 2123b55aab | |||
| 4de6489cbf | |||
| 96c2817e06 | |||
| 35a7ed165f | |||
| 1c5b02fab4 | |||
| 2afec4cc46 | |||
| 6dd6c6af74 | |||
| d86686704c | |||
| 22b8edb023 | |||
| b96deaa0c3 | |||
| 0cb619a787 | |||
| 63951ced9e | |||
| 84502f4c9f | |||
| 430cc64fdb | |||
| b93c733e7c | |||
| fe58e5e92a | |||
| e6ae17cdd5 | |||
| b445153444 | |||
| 6f85747d92 | |||
| 66360c2379 | |||
| 7fe504aa8a | |||
| aca831e54d | |||
| 7da4b1d63c | |||
| f21bda0de9 | |||
| 24ffedd599 | |||
| 7f9acccce7 | |||
| 084fb39abd | |||
| 06694f2428 | |||
| 9105ec6b0d | |||
| 9cfe49dec3 | |||
| 58fb397e79 | |||
| 5de4330199 | |||
| 5669debd6b | |||
| e71335f9b6 | |||
| 24cb5823ee | |||
| 1470a92556 | |||
| 1d98a657b2 | |||
| 2e1f6f0323 | |||
| 04f247905a | |||
| 2bfed74851 | |||
| 2a23b6afdd | |||
| df70f0c824 | |||
| 2285a3fb18 | |||
| ef5447d2fa | |||
| fb64b3ba43 | |||
| 1673201916 | |||
| 72475cd29b | |||
| 41b9eb1897 | |||
| 31db195087 | |||
| 9a9d68c78d | |||
| a2b57c5165 | |||
| e9efab0d59 | |||
| 5d58083ee5 | |||
| 055c7d3c20 | |||
| 0b5ef30b34 | |||
| 6d4ca4ffc0 | |||
| 4cd53c4083 | |||
| c6303cdb6b | |||
| c48e87e012 | |||
| 1ca84bf052 | |||
| f86dac2172 | |||
| 59fe0a058e | |||
| 640a4339db | |||
| 505cf48b6c | |||
| 6d5574cac0 | |||
| e35264708a | |||
| ea0350bcca | |||
| b47ac6dd8a | |||
| 120ed36deb | |||
| 26fe3114a6 | |||
| 39e58acade | |||
| 807e7394fe | |||
| d745be9c96 | |||
| 8f8f6c23ea | |||
| 3da0e529c6 | |||
| d5a862d904 | |||
| 4de2b7f5a8 | |||
| 9f31c61a18 | |||
| d8539c0814 | |||
| 9b8c0b9cf0 | |||
| c4764c0e5b | |||
| aec170d7f8 | |||
| a395156556 | |||
| 50ea3e9a8b | |||
| 50ef79677e | |||
| def778dbf1 | |||
| b8f4401878 | |||
| 9a7fb4a219 | |||
| 39b52eb17e | |||
| 609f1e9655 | |||
| 9bb60d0ae3 | |||
| 202516aee3 | |||
| 489ce76d2a | |||
| 6a8c3c721a | |||
| 21d331c232 | |||
| 4c9d40865f | |||
| 490200b3b8 | |||
| 6031c97e1a | |||
| 4d0777ab93 | |||
| 17308083fe | |||
| 51fb5b9f4a | |||
| 773912320f | |||
| 4a4cd20553 | |||
| 6fbaf46ed9 | |||
| 03da290c54 | |||
| 2d0d6207a1 | |||
| f896b41c6b | |||
| e0d577cbda | |||
| be1c975f4d | |||
| c20773d60b | |||
| 45fb881ec2 | |||
| 64001152ef | |||
| 5aa935b348 | |||
| 826dba7f53 | |||
| 358cfe26e2 | |||
| 8ece805273 | |||
| 8e32816976 | |||
| 64757979e8 | |||
| 26a4861a8b | |||
| 21c6c5a610 | |||
| 5594257147 | |||
| 879d9ec879 | |||
| d13793fcbd | |||
| 51138cbf55 | |||
| 355473a946 | |||
| f25bba7c11 | |||
| f348eb993c | |||
| 3f1675b84a | |||
| 3aac552c44 | |||
| 1717fc0992 | |||
| a7e3deecd3 | |||
| 46c3ea5d22 | |||
| 78f0c1da6a | |||
| 4753766b4f | |||
| 0c1ed01319 | |||
| 91dbc6a7f1 | |||
| d4a46e5361 | |||
| 177a9743d6 | |||
| 2b8338938a | |||
| 84702465d7 | |||
| 3684c87f8c | |||
| 85815ba86d | |||
| 6eb453136d | |||
| 385f4943ae | |||
| 4dcc9609d6 | |||
| 3bbf26a18e | |||
| dfe3aed46e | |||
| 796e080948 | |||
| 052bdefaab | |||
| 794853b7bd | |||
| fbd431164b | |||
| 2c1c1a513a | |||
| 0279b5654a | |||
| c93e907595 | |||
| 5965004721 | |||
| 86d891cfa8 | |||
| 1161fca422 | |||
| 26575ade7e | |||
| fac9b8f54c | |||
| 71ce858b32 | |||
| ff34696d28 | |||
| 2e0214ddb8 | |||
| f316effecd | |||
| 6aa14120de | |||
| 1ad6969d9b | |||
| aed7440c5b | |||
| 10534b46f9 | |||
| 802d4efdd3 | |||
| 20949a0298 | |||
| 8f596f14b0 | |||
| c85043782f | |||
| fe4603f87a | |||
| f8313a04fd | |||
| ba5e85ca67 | |||
| 5233547d76 | |||
| 56db321846 | |||
| 8d0ce8dc49 | |||
| a340f8f31f | |||
| 3853c099d0 | |||
| 35a928e3d8 | |||
| 8d942d0782 | |||
| c70a66b589 | |||
| a8398916c9 | |||
| ed464b196f | |||
| f3f8b82fdd | |||
| 2cd73c265d | |||
| 737e990117 | |||
| 8a78ee090a | |||
| 761aa62995 | |||
| dabf281ae8 | |||
| 5cb9935f2f | |||
| 9236b2f00e | |||
| 29b67f1337 | |||
| dd2c02af3f | |||
| b960441321 | |||
| babb4ca202 | |||
| 4dc1076abc | |||
| 590708da57 | |||
| 78df416bc7 | |||
| fcc054c3ae | |||
| 06b858a77e | |||
| 658b583e84 | |||
| ed557af1c2 | |||
| 61203dc5fd | |||
| b7d417ea91 | |||
| 978405bd85 | |||
| 878115db59 | |||
| 50469e5c82 | |||
| 860e721709 | |||
| 1dbc0cada6 | |||
| 88ece93db2 |
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"dark": {
|
||||
"mPrimary": "#E6B450",
|
||||
"mOnPrimary": "#0B0E14",
|
||||
"mSecondary": "#AAD94C",
|
||||
"mOnSecondary": "#0B0E14",
|
||||
"mTertiary": "#39BAE6",
|
||||
"mOnTertiary": "#0B0E14",
|
||||
"mError": "#D95757",
|
||||
"mOnError": "#0B0E14",
|
||||
"mSurface": "#1e222a",
|
||||
"mOnSurface": "#BFBDB6",
|
||||
"mSurfaceVariant": "#0B0E14",
|
||||
"mOnSurfaceVariant": "#636A72",
|
||||
"mOutline": "#565B66",
|
||||
"mShadow": "#000000"
|
||||
},
|
||||
"light": {
|
||||
"mPrimary": "#FF8F40",
|
||||
"mOnPrimary": "#F8F9FA",
|
||||
"mSecondary": "#86B300",
|
||||
"mOnSecondary": "#F8F9FA",
|
||||
"mTertiary": "#55B4D4",
|
||||
"mOnTertiary": "#F8F9FA",
|
||||
"mError": "#E65050",
|
||||
"mOnError": "#F8F9FA",
|
||||
"mSurface": "#E4E6E9",
|
||||
"mOnSurface": "#5C6166",
|
||||
"mSurfaceVariant": "#F8F9FA",
|
||||
"mOnSurfaceVariant": "#ABADB1",
|
||||
"mOutline": "#8A9199",
|
||||
"mShadow": "#F8F9FA"
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,34 @@
|
||||
{
|
||||
"dark": {
|
||||
"mPrimary": "#c7a1d8",
|
||||
"mOnPrimary": "#1a151f",
|
||||
"mSecondary": "#a984c4",
|
||||
"mOnSecondary": "#f3edf7",
|
||||
"mTertiary": "#e0b7c9",
|
||||
"mOnTertiary": "#20161f",
|
||||
"mError": "#e9899d",
|
||||
"mOnError": "#1e1418",
|
||||
"mSurface": "#1c1822",
|
||||
"mOnSurface": "#e9e4f0",
|
||||
"mSurfaceVariant": "#262130",
|
||||
"mOnSurfaceVariant": "#a79ab0",
|
||||
"mOutline": "#3e364e",
|
||||
"mShadow": "#120f18"
|
||||
"mPrimary": "#fff59b",
|
||||
"mOnPrimary": "#0e0e43",
|
||||
"mSecondary": "#a9aefe",
|
||||
"mOnSecondary": "#0e0e43",
|
||||
"mTertiary": "#9BFECE",
|
||||
"mOnTertiary": "#0e0e43",
|
||||
"mError": "#FD4663",
|
||||
"mOnError": "#0e0e43",
|
||||
"mSurface": "#070722",
|
||||
"mOnSurface": "#f3edf7",
|
||||
"mSurfaceVariant": "#11112d",
|
||||
"mOnSurfaceVariant": "#7c80b4",
|
||||
"mOutline": "#21215F",
|
||||
"mShadow": "#070722"
|
||||
},
|
||||
"light": {
|
||||
"mPrimary": "#9b59ba",
|
||||
"mOnPrimary": "#ffffff",
|
||||
"mSecondary": "#784999",
|
||||
"mOnSecondary": "#ffffff",
|
||||
"mTertiary": "#c17093",
|
||||
"mOnTertiary": "#ffffff",
|
||||
"mError": "#e9899d",
|
||||
"mOnError": "#1e1418",
|
||||
"mSurface": "#f5f1fa",
|
||||
"mOnSurface": "#1c1822",
|
||||
"mSurfaceVariant": "#e7dfee",
|
||||
"mOnSurfaceVariant": "#4a3d59",
|
||||
"mOutline": "#cebedc",
|
||||
"mShadow": "#ffffff"
|
||||
"mPrimary": "#5d65f5",
|
||||
"mOnPrimary": "#dadcff",
|
||||
"mSecondary": "#8E93D8",
|
||||
"mOnSecondary": "#dadcff",
|
||||
"mTertiary": "#0e0e43",
|
||||
"mOnTertiary": "#fef29a",
|
||||
"mError": "#FD4663",
|
||||
"mOnError": "#0e0e43",
|
||||
"mSurface": "#e6e8fa",
|
||||
"mOnSurface": "#4b55c8",
|
||||
"mSurfaceVariant": "#eff0ff",
|
||||
"mOnSurfaceVariant": "#0e0e43",
|
||||
"mOutline": "#8288fc",
|
||||
"mShadow": "#f3edf7"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,23 +1,25 @@
|
||||
palette = 0={{colors.surface.default.hex}}
|
||||
palette = 1={{colors.error.default.hex}}
|
||||
palette = 2={{colors.tertiary.default.hex}}
|
||||
palette = 3={{colors.secondary.default.hex}}
|
||||
palette = 4={{colors.primary.default.hex}}
|
||||
palette = 5={{colors.primary.default.hex}}
|
||||
palette = 6={{colors.secondary.default.hex}}
|
||||
palette = 7={{colors.on_background.default.hex}}
|
||||
palette = 8={{colors.outline.default.hex}}
|
||||
palette = 9={{colors.secondary_fixed_dim.default.hex}}
|
||||
palette = 10={{colors.tertiary_container.default.hex}}
|
||||
palette = 11={{colors.surface_container.default.hex}}
|
||||
palette = 12={{colors.primary_container.default.hex}}
|
||||
palette = 13={{colors.on_primary_container.default.hex}}
|
||||
palette = 14={{colors.surface_variant.default.hex}}
|
||||
palette = 15={{colors.on_background.default.hex}}
|
||||
palette = 0= {{colors.shadow.default.hex}}
|
||||
palette = 1= {{colors.error.default.hex}}
|
||||
palette = 2= {{colors.tertiary.default.hex}}
|
||||
palette = 3= {{colors.secondary.default.hex}}
|
||||
palette = 4= {{colors.primary.default.hex}}
|
||||
palette = 5= {{colors.primary.default.hex}}
|
||||
palette = 6= {{colors.secondary.default.hex}}
|
||||
palette = 7= {{colors.on_background.default.hex}}
|
||||
palette = 8= {{colors.outline.default.hex}}
|
||||
palette = 9= {{colors.secondary_fixed_dim.default.hex}}
|
||||
palette = 10= {{colors.tertiary_container.default.hex}}
|
||||
palette = 11= {{colors.surface_container.default.hex}}
|
||||
palette = 12= {{colors.primary_container.default.hex}}
|
||||
palette = 13= {{colors.on_primary_container.default.hex}}
|
||||
palette = 14= {{colors.surface_variant.default.hex}}
|
||||
palette = 15= {{colors.primary.default.hex}}
|
||||
|
||||
cursor-color = {{colors.primary.default.hex}}
|
||||
foreground={{colors.on_surface.default.hex}}
|
||||
background={{colors.surface.default.hex}}
|
||||
|
||||
cursor-color = {{colors.on_surface.default.hex}}
|
||||
cursor-text = {{colors.on_surface.default.hex}}
|
||||
foreground = {{colors.on_surface.default.hex}}
|
||||
background = {{colors.surface.default.hex}}
|
||||
selection-foreground = {{colors.on_secondary.default.hex}}
|
||||
selection-background = {{colors.secondary_fixed_dim.default.hex}}
|
||||
|
||||
selection-background = {{colors.on_secondary.default.hex}}
|
||||
selection-foreground = {{colors.secondary_fixed_dim.default.hex}}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 760 KiB |
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"settingsVersion": 6,
|
||||
"bar": {
|
||||
"position": "top",
|
||||
"backgroundOpacity": 1,
|
||||
"monitors": [],
|
||||
"density": "default",
|
||||
"showCapsule": true,
|
||||
"floating": false,
|
||||
"marginVertical": 0.25,
|
||||
"marginHorizontal": 0.25,
|
||||
"widgets": {
|
||||
"left": [
|
||||
{
|
||||
"id": "SystemMonitor"
|
||||
},
|
||||
{
|
||||
"id": "ActiveWindow"
|
||||
},
|
||||
{
|
||||
"id": "MediaMini"
|
||||
}
|
||||
],
|
||||
"center": [
|
||||
{
|
||||
"id": "Workspace"
|
||||
}
|
||||
],
|
||||
"right": [
|
||||
{
|
||||
"id": "ScreenRecorder"
|
||||
},
|
||||
{
|
||||
"id": "Tray"
|
||||
},
|
||||
{
|
||||
"id": "NotificationHistory"
|
||||
},
|
||||
{
|
||||
"id": "WiFi"
|
||||
},
|
||||
{
|
||||
"id": "Bluetooth"
|
||||
},
|
||||
{
|
||||
"id": "Battery"
|
||||
},
|
||||
{
|
||||
"id": "Volume"
|
||||
},
|
||||
{
|
||||
"id": "Brightness"
|
||||
},
|
||||
{
|
||||
"id": "NightLight"
|
||||
},
|
||||
{
|
||||
"id": "Clock"
|
||||
},
|
||||
{
|
||||
"id": "ControlCenter"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"avatarImage": "",
|
||||
"dimDesktop": true,
|
||||
"showScreenCorners": false,
|
||||
"forceBlackScreenCorners": false,
|
||||
"radiusRatio": 1,
|
||||
"screenRadiusRatio": 1,
|
||||
"animationSpeed": 1
|
||||
},
|
||||
"location": {
|
||||
"name": "Tokyo",
|
||||
"useFahrenheit": false,
|
||||
"use12hourFormat": false,
|
||||
"showWeekNumberInCalendar": false
|
||||
},
|
||||
"screenRecorder": {
|
||||
"directory": "",
|
||||
"frameRate": 60,
|
||||
"audioCodec": "opus",
|
||||
"videoCodec": "h264",
|
||||
"quality": "very_high",
|
||||
"colorRange": "limited",
|
||||
"showCursor": true,
|
||||
"audioSource": "default_output",
|
||||
"videoSource": "portal"
|
||||
},
|
||||
"wallpaper": {
|
||||
"enabled": true,
|
||||
"directory": "",
|
||||
"enableMultiMonitorDirectories": false,
|
||||
"setWallpaperOnAllMonitors": true,
|
||||
"fillMode": "crop",
|
||||
"fillColor": "#000000",
|
||||
"randomEnabled": false,
|
||||
"randomIntervalSec": 300,
|
||||
"transitionDuration": 1500,
|
||||
"transitionType": "random",
|
||||
"transitionEdgeSmoothness": 0.05,
|
||||
"monitors": []
|
||||
},
|
||||
"appLauncher": {
|
||||
"enableClipboardHistory": false,
|
||||
"position": "center",
|
||||
"backgroundOpacity": 1,
|
||||
"pinnedExecs": [],
|
||||
"useApp2Unit": false,
|
||||
"sortByMostUsed": true
|
||||
},
|
||||
"dock": {
|
||||
"autoHide": false,
|
||||
"exclusive": false,
|
||||
"backgroundOpacity": 1,
|
||||
"floatingRatio": 1,
|
||||
"monitors": [],
|
||||
"pinnedApps": []
|
||||
},
|
||||
"network": {
|
||||
"wifiEnabled": true,
|
||||
"bluetoothEnabled": true
|
||||
},
|
||||
"notifications": {
|
||||
"doNotDisturb": false,
|
||||
"monitors": [],
|
||||
"location": "top_right",
|
||||
"alwaysOnTop": false,
|
||||
"lastSeenTs": 0,
|
||||
"respectExpireTimeout": false,
|
||||
"lowUrgencyDuration": 3,
|
||||
"normalUrgencyDuration": 8,
|
||||
"criticalUrgencyDuration": 15,
|
||||
"enableOSD": true
|
||||
},
|
||||
"audio": {
|
||||
"volumeStep": 5,
|
||||
"volumeOverdrive": false,
|
||||
"cavaFrameRate": 60,
|
||||
"visualizerType": "linear",
|
||||
"mprisBlacklist": [],
|
||||
"preferredPlayer": ""
|
||||
},
|
||||
"ui": {
|
||||
"fontDefault": "Roboto",
|
||||
"fontFixed": "DejaVu Sans Mono",
|
||||
"fontBillboard": "Inter",
|
||||
"monitorsScaling": [],
|
||||
"idleInhibitorEnabled": false
|
||||
},
|
||||
"brightness": {
|
||||
"brightnessStep": 5
|
||||
},
|
||||
"colorSchemes": {
|
||||
"useWallpaperColors": false,
|
||||
"predefinedScheme": "Noctalia (default)",
|
||||
"darkMode": true
|
||||
},
|
||||
"matugen": {
|
||||
"gtk4": false,
|
||||
"gtk3": false,
|
||||
"qt6": false,
|
||||
"qt5": false,
|
||||
"kitty": false,
|
||||
"ghostty": false,
|
||||
"foot": false,
|
||||
"fuzzel": false,
|
||||
"vesktop": false,
|
||||
"pywalfox": false,
|
||||
"enableUserTemplates": false
|
||||
},
|
||||
"nightLight": {
|
||||
"enabled": false,
|
||||
"forced": false,
|
||||
"autoSchedule": true,
|
||||
"nightTemp": "4000",
|
||||
"dayTemp": "6500",
|
||||
"manualSunrise": "06:30",
|
||||
"manualSunset": "18:30"
|
||||
},
|
||||
"hooks": {
|
||||
"enabled": false,
|
||||
"wallpaperChange": "",
|
||||
"darkModeChange": ""
|
||||
}
|
||||
}
|
||||
Executable
+424
@@ -0,0 +1,424 @@
|
||||
#!/bin/bash
|
||||
|
||||
# JSON Language File Comparison Script
|
||||
# Compares language files against en.json reference and generates a report
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
FOLDER_PATH="Assets/Translations"
|
||||
REFERENCE_FILE="en.json"
|
||||
|
||||
# Colors for terminal output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_color() {
|
||||
local color=$1
|
||||
local message=$2
|
||||
echo -e "${color}${message}${NC}"
|
||||
}
|
||||
|
||||
# Function to check if jq is installed
|
||||
check_dependencies() {
|
||||
if ! command -v jq &> /dev/null; then
|
||||
print_color $RED "Error: 'jq' is required but not installed. Please install jq first." >&2
|
||||
print_color $YELLOW "On Ubuntu/Debian: sudo apt-get install jq" >&2
|
||||
print_color $YELLOW "On CentOS/RHEL: sudo yum install jq" >&2
|
||||
print_color $YELLOW "On macOS: brew install jq" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to extract all keys from a JSON file recursively
|
||||
extract_keys() {
|
||||
local json_file=$1
|
||||
|
||||
if [[ ! -f "$json_file" ]]; then
|
||||
echo "Error: File $json_file not found" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract all keys recursively using jq
|
||||
jq -r '
|
||||
def keys_recursive:
|
||||
if type == "object" then
|
||||
keys[] as $k |
|
||||
if (.[$k] | type) == "object" then
|
||||
($k + "." + (.[$k] | keys_recursive))
|
||||
else
|
||||
$k
|
||||
end
|
||||
else
|
||||
empty
|
||||
end;
|
||||
keys_recursive
|
||||
' "$json_file" 2>/dev/null | sort
|
||||
}
|
||||
|
||||
# Function to get language files
|
||||
get_language_files() {
|
||||
find "$FOLDER_PATH" -maxdepth 1 -name "*.json" -type f | sort
|
||||
}
|
||||
|
||||
# Function to generate report header
|
||||
generate_header() {
|
||||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
echo "================================================================================"
|
||||
echo " LANGUAGE FILE COMPARISON REPORT"
|
||||
echo "================================================================================"
|
||||
echo "Generated: $timestamp"
|
||||
echo "Reference file: $REFERENCE_FILE"
|
||||
echo "Folder: $(realpath "$FOLDER_PATH")"
|
||||
echo ""
|
||||
echo "Notes:"
|
||||
echo "- Keys are compared recursively through all nested JSON objects"
|
||||
echo "- Missing keys indicate incomplete translations"
|
||||
echo "- Extra keys might indicate deprecated keys or translation-specific additions"
|
||||
echo "- Translation completion percentage is calculated based on English reference"
|
||||
echo "- Results are sorted by descending line numbers for easier editing"
|
||||
echo ""
|
||||
echo "This report compares all language JSON files against the English reference file"
|
||||
echo "and identifies missing keys and extra keys in each language."
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Function to find line number of a key in JSON file
|
||||
find_key_line_number() {
|
||||
local json_file=$1
|
||||
local key_path=$2
|
||||
|
||||
# Extract the final key name (after last dot)
|
||||
local key_name="${key_path##*.}"
|
||||
|
||||
# Search for the key in the file with line numbers
|
||||
# Look for the pattern "key": (with quotes and colon)
|
||||
local line_num=$(grep -n "\"$key_name\":" "$json_file" 2>/dev/null | head -1 | cut -d: -f1 || echo "")
|
||||
|
||||
if [[ -n "$line_num" ]]; then
|
||||
echo "$line_num"
|
||||
else
|
||||
# If not found with quotes, try without (though less reliable)
|
||||
line_num=$(grep -n "$key_name:" "$json_file" 2>/dev/null | head -1 | cut -d: -f1 || echo "")
|
||||
if [[ -n "$line_num" ]]; then
|
||||
echo "$line_num"
|
||||
else
|
||||
echo "?"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to safely count lines
|
||||
count_non_empty_lines() {
|
||||
local content="$1"
|
||||
if [[ -z "$content" ]]; then
|
||||
echo "0"
|
||||
else
|
||||
echo "$content" | grep -c -v '^$' || echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to compare keys and generate report section
|
||||
compare_language() {
|
||||
local lang_file="$1"
|
||||
local lang_name="$2"
|
||||
local ref_keys_file="$3"
|
||||
local ref_file_path="$FOLDER_PATH/$REFERENCE_FILE"
|
||||
|
||||
# Create temporary file for language keys
|
||||
local lang_keys_file=$(mktemp)
|
||||
extract_keys "$lang_file" > "$lang_keys_file" || {
|
||||
echo "Error: Failed to extract keys from $lang_file" >&2
|
||||
rm -f "$lang_keys_file"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Get missing and extra keys safely
|
||||
local missing_keys=""
|
||||
local extra_keys=""
|
||||
|
||||
missing_keys=$(comm -23 "$ref_keys_file" "$lang_keys_file" 2>/dev/null || echo "")
|
||||
extra_keys=$(comm -13 "$ref_keys_file" "$lang_keys_file" 2>/dev/null || echo "")
|
||||
|
||||
# Count lines safely
|
||||
local missing_count=$(count_non_empty_lines "$missing_keys")
|
||||
local extra_count=$(count_non_empty_lines "$extra_keys")
|
||||
local total_ref_keys=$(wc -l < "$ref_keys_file" 2>/dev/null || echo "0")
|
||||
local total_lang_keys=$(wc -l < "$lang_keys_file" 2>/dev/null || echo "0")
|
||||
|
||||
# Calculate completion percentage safely
|
||||
local completion_percentage=0
|
||||
if [[ $total_ref_keys -gt 0 ]]; then
|
||||
completion_percentage=$(( (total_ref_keys - missing_count) * 100 / total_ref_keys ))
|
||||
fi
|
||||
|
||||
print_color $YELLOW "================================================================================"
|
||||
print_color $YELLOW "LANGUAGE: $lang_name"
|
||||
print_color $YELLOW "================================================================================"
|
||||
echo "File: $lang_file"
|
||||
echo "Total keys in reference (en): $total_ref_keys"
|
||||
echo "Total keys in $lang_name: $total_lang_keys"
|
||||
|
||||
# Color code the completion percentage
|
||||
if [[ $completion_percentage -eq 100 ]]; then
|
||||
echo -e "Translation completion: ${GREEN}${completion_percentage}%${NC}"
|
||||
else
|
||||
echo -e "Translation completion: ${RED}${completion_percentage}%${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "SUMMARY:"
|
||||
echo "- Missing keys (exist in English but not in $lang_name): $missing_count"
|
||||
echo "- Extra keys (exist in $lang_name but not in English): $extra_count"
|
||||
echo ""
|
||||
|
||||
# Handle missing keys
|
||||
if [[ $missing_count -gt 0 && -n "$missing_keys" ]]; then
|
||||
echo "MISSING KEYS IN $lang_name:"
|
||||
|
||||
# Collect keys with line numbers and sort by line number (descending)
|
||||
local temp_missing=$(mktemp)
|
||||
while IFS= read -r key; do
|
||||
if [[ -n "$key" ]]; then
|
||||
local ref_line=$(find_key_line_number "$ref_file_path" "$key")
|
||||
# Use numeric sort padding for proper sorting
|
||||
if [[ "$ref_line" =~ ^[0-9]+$ ]]; then
|
||||
printf "%06d|%s|en.json:%s\n" "$ref_line" "$key" "$ref_line" >> "$temp_missing"
|
||||
else
|
||||
printf "999999|%s|en.json:%s\n" "$key" "$ref_line" >> "$temp_missing"
|
||||
fi
|
||||
fi
|
||||
done <<< "$missing_keys"
|
||||
|
||||
# Sort by line number (descending) and display
|
||||
local counter=1
|
||||
sort -t'|' -k1,1nr "$temp_missing" | while IFS='|' read -r sort_key key location; do
|
||||
printf " %3d. %s (%s)\n" "$counter" "$key" "$location"
|
||||
counter=$((counter + 1))
|
||||
done
|
||||
rm -f "$temp_missing"
|
||||
echo ""
|
||||
else
|
||||
echo "✅ No missing keys in $lang_name"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Handle extra keys
|
||||
if [[ $extra_count -gt 0 && -n "$extra_keys" ]]; then
|
||||
echo "EXTRA KEYS IN $lang_name (not in English):"
|
||||
|
||||
# Collect keys with line numbers and sort by line number (descending)
|
||||
local temp_extra=$(mktemp)
|
||||
while IFS= read -r key; do
|
||||
if [[ -n "$key" ]]; then
|
||||
local lang_line=$(find_key_line_number "$lang_file" "$key")
|
||||
# Use numeric sort padding for proper sorting
|
||||
if [[ "$lang_line" =~ ^[0-9]+$ ]]; then
|
||||
printf "%06d|%s|%s:%s\n" "$lang_line" "$key" "$(basename "$lang_file")" "$lang_line" >> "$temp_extra"
|
||||
else
|
||||
printf "999999|%s|%s:%s\n" "$key" "$(basename "$lang_file")" "$lang_line" >> "$temp_extra"
|
||||
fi
|
||||
fi
|
||||
done <<< "$extra_keys"
|
||||
|
||||
# Sort by line number (descending) and display
|
||||
local counter=1
|
||||
sort -t'|' -k1,1nr "$temp_extra" | while IFS='|' read -r sort_key key location; do
|
||||
printf " %3d. %s (%s)\n" "$counter" "$key" "$location"
|
||||
counter=$((counter + 1))
|
||||
done
|
||||
rm -f "$temp_extra"
|
||||
echo ""
|
||||
else
|
||||
echo "✅ No extra keys in $lang_name"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm -f "$lang_keys_file"
|
||||
}
|
||||
|
||||
# Main function
|
||||
main() {
|
||||
local target_language="$1"
|
||||
|
||||
print_color $BLUE "Starting language file comparison..." >&2
|
||||
|
||||
# Check dependencies
|
||||
check_dependencies
|
||||
|
||||
# Validate folder path
|
||||
if [[ ! -d "$FOLDER_PATH" ]]; then
|
||||
print_color $RED "Error: Folder '$FOLDER_PATH' does not exist" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if reference file exists
|
||||
local ref_file_path="$FOLDER_PATH/$REFERENCE_FILE"
|
||||
if [[ ! -f "$ref_file_path" ]]; then
|
||||
print_color $RED "Error: Reference file '$ref_file_path' does not exist" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_color $GREEN "Reference file found: $ref_file_path" >&2
|
||||
|
||||
# Extract keys from reference file
|
||||
local ref_keys_file=$(mktemp)
|
||||
if ! extract_keys "$ref_file_path" > "$ref_keys_file"; then
|
||||
print_color $RED "Error: Failed to extract keys from reference file" >&2
|
||||
rm -f "$ref_keys_file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local total_ref_keys=$(wc -l < "$ref_keys_file" 2>/dev/null || echo "0")
|
||||
|
||||
print_color $BLUE "Extracted $total_ref_keys keys from reference file" >&2
|
||||
|
||||
# Get all language files or just the target language
|
||||
local -a language_files
|
||||
if [[ -n "$target_language" ]]; then
|
||||
# Single language mode
|
||||
local target_file="$FOLDER_PATH/${target_language}.json"
|
||||
if [[ ! -f "$target_file" ]]; then
|
||||
print_color $RED "Error: Language file '$target_file' does not exist" >&2
|
||||
rm -f "$ref_keys_file"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$target_language" == "${REFERENCE_FILE%.json}" ]]; then
|
||||
print_color $RED "Error: Cannot compare reference file against itself" >&2
|
||||
rm -f "$ref_keys_file"
|
||||
exit 1
|
||||
fi
|
||||
language_files=("$target_file")
|
||||
print_color $BLUE "Checking single language: $target_language" >&2
|
||||
else
|
||||
# All languages mode
|
||||
while IFS= read -r -d '' file; do
|
||||
language_files+=("$file")
|
||||
done < <(find "$FOLDER_PATH" -maxdepth 1 -name "*.json" -type f -print0 | sort -z)
|
||||
|
||||
if [[ ${#language_files[@]} -eq 0 ]]; then
|
||||
print_color $RED "Error: No JSON files found in $FOLDER_PATH" >&2
|
||||
rm -f "$ref_keys_file"
|
||||
exit 1
|
||||
fi
|
||||
print_color $BLUE "Found ${#language_files[@]} JSON files to process" >&2
|
||||
fi
|
||||
|
||||
echo "" >&2
|
||||
|
||||
# Generate report header
|
||||
generate_header
|
||||
|
||||
local processed=0
|
||||
for lang_file in "${language_files[@]}"; do
|
||||
local filename=$(basename "$lang_file")
|
||||
local lang_name="${filename%.json}"
|
||||
|
||||
# Skip the reference file in all-languages mode
|
||||
if [[ -z "$target_language" && "$filename" == "$REFERENCE_FILE" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
print_color $YELLOW "Processing: $filename" >&2
|
||||
|
||||
# Validate JSON syntax
|
||||
if ! jq empty "$lang_file" 2>/dev/null; then
|
||||
print_color $RED "Warning: $lang_file contains invalid JSON syntax. Skipping..." >&2
|
||||
echo "ERROR: $lang_file contains invalid JSON syntax and was skipped."
|
||||
echo ""
|
||||
continue
|
||||
fi
|
||||
|
||||
if compare_language "$lang_file" "$lang_name" "$ref_keys_file"; then
|
||||
processed=$((processed + 1))
|
||||
else
|
||||
print_color $RED "Error processing $lang_file" >&2
|
||||
fi
|
||||
done
|
||||
|
||||
# Add summary at the end
|
||||
echo "================================================================================"
|
||||
echo "SUMMARY"
|
||||
echo "================================================================================"
|
||||
echo "Total files processed: $processed"
|
||||
echo "Reference file: $REFERENCE_FILE (English)"
|
||||
if [[ -n "$target_language" ]]; then
|
||||
echo "Target language: $target_language"
|
||||
fi
|
||||
echo "Report generated: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo ""
|
||||
echo "================================================================================"
|
||||
|
||||
# Clean up
|
||||
rm -f "$ref_keys_file"
|
||||
|
||||
if [[ -n "$target_language" ]]; then
|
||||
print_color $GREEN "Comparison completed for language: $target_language" >&2
|
||||
else
|
||||
print_color $GREEN "Comparison completed: Processed $processed language files against English reference" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
# Usage information
|
||||
show_usage() {
|
||||
echo "Usage: $0 [language_code]" >&2
|
||||
echo "" >&2
|
||||
echo "This script compares JSON language files in '$FOLDER_PATH' against the English reference." >&2
|
||||
echo "" >&2
|
||||
echo "Arguments:" >&2
|
||||
echo " language_code Optional. Compare only the specified language (e.g., 'fr', 'es', 'de')" >&2
|
||||
echo " If not provided, all language files will be compared" >&2
|
||||
echo "" >&2
|
||||
echo "Configuration:" >&2
|
||||
echo " - Folder path: $FOLDER_PATH (hardcoded)" >&2
|
||||
echo " - Reference file: $REFERENCE_FILE" >&2
|
||||
echo "" >&2
|
||||
echo "Examples:" >&2
|
||||
echo " $0 # Compare all languages" >&2
|
||||
echo " $0 fr # Compare only French (fr.json)" >&2
|
||||
echo " $0 es # Compare only Spanish (es.json)" >&2
|
||||
echo "" >&2
|
||||
echo "Requirements:" >&2
|
||||
echo " - jq must be installed" >&2
|
||||
echo " - $REFERENCE_FILE must exist in $FOLDER_PATH" >&2
|
||||
echo " - Target language file must exist if specified" >&2
|
||||
echo "" >&2
|
||||
echo "Output:" >&2
|
||||
echo " - Comparison report is printed to stdout" >&2
|
||||
echo " - Progress messages are printed to stderr" >&2
|
||||
echo " - Results are sorted by descending line numbers for easier editing" >&2
|
||||
}
|
||||
|
||||
# Handle command line arguments
|
||||
if [[ $# -gt 1 ]]; then
|
||||
echo "Error: Too many arguments. Only one language code is allowed." >&2
|
||||
echo "" >&2
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# -eq 1 ]]; then
|
||||
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
|
||||
show_usage
|
||||
exit 0
|
||||
else
|
||||
# Validate language code format (basic check for reasonable filename)
|
||||
if [[ ! "$1" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]]; then
|
||||
echo "Error: Invalid language code format '$1'. Use alphanumeric characters, hyphens, and underscores only." >&2
|
||||
echo "" >&2
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
# Run main function with target language
|
||||
main "$1"
|
||||
fi
|
||||
else
|
||||
# Run main function for all languages
|
||||
main ""
|
||||
fi
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Comprehensive i18n checker for QML files
|
||||
# Finds hardcoded strings in various QML properties
|
||||
|
||||
find . -name "*.qml" -type f | while read -r file; do
|
||||
# Skip if file doesn't exist or is not readable
|
||||
[[ ! -r "$file" ]] && continue
|
||||
|
||||
# Check for hardcoded strings in common properties
|
||||
# Matches: property: "text with letters" but excludes I18n.tr calls
|
||||
issues=$(grep -n -E '(label|text|title|description|placeholder|tooltipText|tooltip):\s*"[^"]*[a-zA-Z][^"]*"' "$file" | grep -v 'I18n\.tr')
|
||||
|
||||
# Also check for template literals with hardcoded text
|
||||
template_issues=$(grep -n -E '(label|text|title|description|placeholder|tooltipText|tooltip):\s*`[^`]*[a-zA-Z][^`]*`' "$file" | grep -v 'I18n\.tr')
|
||||
|
||||
# Check for property assignments with hardcoded strings
|
||||
property_issues=$(grep -n -E 'property\s+string\s+\w+:\s*"[^"]*[a-zA-Z][^"]*"' "$file" | grep -v 'I18n\.tr')
|
||||
|
||||
# Check for JavaScript object properties with hardcoded strings (like in arrays/models)
|
||||
js_object_issues=$(grep -n -E '"(label|text|title|description|placeholder|name)":\s*"[^"]*[a-zA-Z][^"]*"' "$file" | grep -v 'I18n\.tr')
|
||||
|
||||
if [[ -n "$issues" || -n "$template_issues" || -n "$property_issues" || -n "$js_object_issues" ]]; then
|
||||
echo "$file"
|
||||
[[ -n "$issues" ]] && echo "$issues"
|
||||
[[ -n "$template_issues" ]] && echo "$template_issues"
|
||||
[[ -n "$property_issues" ]] && echo "$property_issues"
|
||||
[[ -n "$js_object_issues" ]] && echo "$js_object_issues"
|
||||
echo
|
||||
fi
|
||||
done
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env -S bash
|
||||
|
||||
echo "Sending test notifications..."
|
||||
|
||||
# Send a bunch of notifications with numbers
|
||||
for i in {1..4}; do
|
||||
notify-send "Notification $i" "This is test notification number $i with a very long text that will probably break the layout or maybe not? Who knows? Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "All notifications sent!"
|
||||
|
||||
# Additional tests for icon/image handling
|
||||
if command -v notify-send >/dev/null 2>&1; then
|
||||
echo "Sending icon/image tests..."
|
||||
|
||||
# 1) Themed icon name
|
||||
notify-send -i dialog-information "Icon name test" "Should resolve from theme (dialog-information)"
|
||||
|
||||
# 2) Absolute path if a sample image exists
|
||||
SAMPLE_IMG="/usr/share/pixmaps/steam.png"
|
||||
if [ -f "$SAMPLE_IMG" ]; then
|
||||
notify-send -i "$SAMPLE_IMG" "Absolute path test" "Should show the provided image path"
|
||||
fi
|
||||
|
||||
# 3) file:// URL form
|
||||
if [ -f "$SAMPLE_IMG" ]; then
|
||||
notify-send -i "file://$SAMPLE_IMG" "file:// URL test" "Should display after stripping scheme"
|
||||
fi
|
||||
|
||||
echo "Icon/image tests sent!"
|
||||
fi
|
||||
|
||||
# A test notification with actions
|
||||
gdbus call --session \
|
||||
--dest org.freedesktop.Notifications \
|
||||
--object-path /org/freedesktop/Notifications \
|
||||
--method org.freedesktop.Notifications.Notify \
|
||||
"my-app" \
|
||||
0 \
|
||||
"dialog-question" \
|
||||
"Confirmation Required" \
|
||||
"Do you want to proceed with the action?" \
|
||||
"['default', 'OK', 'cancel', 'Cancel']" \
|
||||
"{}" \
|
||||
5000
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/usr/bin/env -S bash
|
||||
|
||||
echo "Sending 8 test notifications..."
|
||||
|
||||
# Send 8 notifications with numbers
|
||||
for i in {1..8}; do
|
||||
notify-send "Notification $i" "This is test notification number $i of 8"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "All notifications sent!"
|
||||
|
||||
# Additional tests for icon/image handling
|
||||
if command -v notify-send >/dev/null 2>&1; then
|
||||
echo "Sending icon/image tests..."
|
||||
|
||||
# 1) Themed icon name
|
||||
notify-send -i dialog-information "Icon name test" "Should resolve from theme (dialog-information)"
|
||||
|
||||
# 2) Absolute path if a sample image exists
|
||||
SAMPLE_IMG="/usr/share/pixmaps/debian-logo.png"
|
||||
if [ -f "$SAMPLE_IMG" ]; then
|
||||
notify-send -i "$SAMPLE_IMG" "Absolute path test" "Should show the provided image path"
|
||||
fi
|
||||
|
||||
# 3) file:// URL form
|
||||
if [ -f "$SAMPLE_IMG" ]; then
|
||||
notify-send -i "file://$SAMPLE_IMG" "file:// URL test" "Should display after stripping scheme"
|
||||
fi
|
||||
|
||||
echo "Icon/image tests sent!"
|
||||
fi
|
||||
@@ -0,0 +1,351 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool debug: false
|
||||
property string debugForceLanguage: ""
|
||||
|
||||
property bool isLoaded: false
|
||||
property string langCode: ""
|
||||
property var availableLanguages: []
|
||||
property var translations: ({})
|
||||
property var fallbackTranslations: ({})
|
||||
|
||||
// Signals for reactive updates
|
||||
signal languageChanged(string newLanguage)
|
||||
signal translationsLoaded
|
||||
|
||||
// Process to list directory contents
|
||||
property Process directoryScanner: Process {
|
||||
id: directoryProcess
|
||||
command: ["ls", `${Quickshell.shellDir}/Assets/Translations/`]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
id: stdoutCollector
|
||||
}
|
||||
|
||||
onExited: function (exitCode, exitStatus) {
|
||||
if (exitCode === 0) {
|
||||
var output = stdoutCollector.text || ""
|
||||
parseDirectoryListing(output)
|
||||
} else {
|
||||
Logger.error("I18n", `Failed to scan translation directory`)
|
||||
// Fallback to default languages
|
||||
availableLanguages = ["en"]
|
||||
detectLanguage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FileView to load translation files
|
||||
property FileView translationFile: FileView {
|
||||
id: fileView
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onLoaded: {
|
||||
try {
|
||||
var data = JSON.parse(text())
|
||||
root.translations = data
|
||||
root.isLoaded = true
|
||||
root.translationsLoaded()
|
||||
Logger.log("I18n", `Loaded translations for "${root.langCode}"`)
|
||||
} catch (e) {
|
||||
Logger.error("I18n", `Failed to parse translation file: ${e}`)
|
||||
setLanguage("en")
|
||||
}
|
||||
}
|
||||
onLoadFailed: function (error) {
|
||||
setLanguage("en")
|
||||
Logger.error("I18n", `Failed to load translation file: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
// FileView to load fallback translation files
|
||||
property FileView fallbackTranslationFile: FileView {
|
||||
id: fallbackFileView
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onLoaded: {
|
||||
try {
|
||||
var data = JSON.parse(text())
|
||||
root.fallbackTranslations = data
|
||||
Logger.log("I18n", `Loaded english fallback translations`)
|
||||
} catch (e) {
|
||||
Logger.error("I18n", `Failed to parse fallback translation file: ${e}`)
|
||||
}
|
||||
}
|
||||
onLoadFailed: function (error) {
|
||||
Logger.error("I18n", `Failed to load fallback translation file: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.log("I18n", "Service started")
|
||||
scanAvailableLanguages()
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
function scanAvailableLanguages() {
|
||||
Logger.log("I18n", "Scanning for available translation files...")
|
||||
directoryScanner.running = true
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
function parseDirectoryListing(output) {
|
||||
var languages = []
|
||||
|
||||
try {
|
||||
if (!output || output.trim() === "") {
|
||||
Logger.warn("I18n", "Empty directory listing output")
|
||||
availableLanguages = ["en"]
|
||||
detectLanguage()
|
||||
return
|
||||
}
|
||||
|
||||
const entries = output.trim().split('\n')
|
||||
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i].trim()
|
||||
if (entry && entry.endsWith('.json')) {
|
||||
// Extract language code from filename (e.g., "en.json" -> "en")
|
||||
const langCode = entry.substring(0, entry.lastIndexOf('.json'))
|
||||
if (langCode.length >= 2 && langCode.length <= 5) {
|
||||
// Basic validation for language codes
|
||||
languages.push(langCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort languages alphabetically, but ensure "en" comes first if available
|
||||
languages.sort()
|
||||
const enIndex = languages.indexOf("en")
|
||||
if (enIndex > 0) {
|
||||
languages.splice(enIndex, 1)
|
||||
languages.unshift("en")
|
||||
}
|
||||
|
||||
if (languages.length === 0) {
|
||||
Logger.warn("I18n", "No translation files found, using fallback")
|
||||
languages = ["en"] // Fallback
|
||||
}
|
||||
|
||||
availableLanguages = languages
|
||||
Logger.log("I18n", `Found ${languages.length} available languages: ${languages.join(', ')}`)
|
||||
|
||||
// Detect language after scanning
|
||||
detectLanguage()
|
||||
} catch (e) {
|
||||
Logger.error("I18n", `Failed to parse directory listing: ${e}`)
|
||||
// Fallback to default languages
|
||||
availableLanguages = ["en"]
|
||||
detectLanguage()
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
function detectLanguage() {
|
||||
Logger.log("I18n", `detectLanguage() called. Available languages: [${availableLanguages.join(', ')}]`)
|
||||
|
||||
if (availableLanguages.length === 0) {
|
||||
Logger.warn("I18n", "No available languages found")
|
||||
return
|
||||
}
|
||||
|
||||
if (debug && debugForceLanguage !== "") {
|
||||
Logger.log("I18n", `Debug mode: forcing language to "${debugForceLanguage}"`)
|
||||
if (availableLanguages.includes(debugForceLanguage)) {
|
||||
setLanguage(debugForceLanguage)
|
||||
return
|
||||
} else {
|
||||
Logger.warn("I18n", `Debug language "${debugForceLanguage}" not available in [${availableLanguages.join(', ')}]`)
|
||||
}
|
||||
}
|
||||
|
||||
// Detect user's favorite locale - languages
|
||||
for (var i = 0; i < Qt.locale().uiLanguages.length; i++) {
|
||||
const userLang = Qt.locale().uiLanguages[i].substring(0, 2)
|
||||
if (availableLanguages.includes(userLang)) {
|
||||
setLanguage(userLang)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to first available language (preferably "en" if available)
|
||||
const fallbackLang = availableLanguages.includes("en") ? "en" : availableLanguages[0]
|
||||
setLanguage(fallbackLang)
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
function setLanguage(newLangCode) {
|
||||
if (newLangCode !== langCode && availableLanguages.includes(newLangCode)) {
|
||||
langCode = newLangCode
|
||||
Logger.log("I18n", `Language set to "${langCode}"`)
|
||||
languageChanged(langCode)
|
||||
loadTranslations()
|
||||
} else if (!availableLanguages.includes(newLangCode)) {
|
||||
Logger.warn("I18n", `Language "${newLangCode}" is not available`)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
function loadTranslations() {
|
||||
if (langCode === "")
|
||||
return
|
||||
|
||||
const filePath = `file://${Quickshell.shellDir}/Assets/Translations/${langCode}.json`
|
||||
fileView.path = filePath
|
||||
isLoaded = false
|
||||
Logger.log("I18n", `Loading translations from: ${filePath}`)
|
||||
|
||||
// Only load fallback translations if we are not using english and english is available
|
||||
if (langCode !== "en" && availableLanguages.includes("en")) {
|
||||
fallbackFileView.path = `file://${Quickshell.shellDir}/Assets/Translations/en.json`
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Check if a translation exists
|
||||
function hasTranslation(key) {
|
||||
if (!isLoaded)
|
||||
return false
|
||||
|
||||
const keys = key.split(".")
|
||||
var value = translations
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (value && typeof value === "object" && keys[i] in value) {
|
||||
value = value[keys[i]]
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return typeof value === "string"
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Get all translation keys (useful for debugging)
|
||||
function getAllKeys(obj, prefix) {
|
||||
if (typeof obj === "undefined")
|
||||
obj = translations
|
||||
if (typeof prefix === "undefined")
|
||||
prefix = ""
|
||||
|
||||
var keys = []
|
||||
for (var key in (obj || {})) {
|
||||
const value = obj[key]
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key
|
||||
if (typeof value === "object" && value !== null) {
|
||||
keys = keys.concat(getAllKeys(value, fullKey))
|
||||
} else if (typeof value === "string") {
|
||||
keys.push(fullKey)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Reload translations (useful for development)
|
||||
function reload() {
|
||||
Logger.log("I18n", "Reloading translations")
|
||||
loadTranslations()
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Main translation function
|
||||
function tr(key, interpolations) {
|
||||
if (typeof interpolations === "undefined")
|
||||
interpolations = {}
|
||||
|
||||
if (!isLoaded) {
|
||||
// if (debug) {
|
||||
// Logger.warn("I18n", "Translations not loaded yet")
|
||||
// }
|
||||
return key
|
||||
}
|
||||
|
||||
// Navigate nested keys (e.g., "menu.file.open")
|
||||
const keys = key.split(".")
|
||||
|
||||
// Look-up translation in the active language
|
||||
var value = translations
|
||||
var notFound = false
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (value && typeof value === "object" && keys[i] in value) {
|
||||
value = value[keys[i]]
|
||||
} else {
|
||||
if (debug) {
|
||||
Logger.warn("I18n", `Translation key "${key}" not found`)
|
||||
}
|
||||
notFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to english if not found
|
||||
if (notFound && availableLanguages.includes("en") && langCode !== "en") {
|
||||
value = fallbackTranslations
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (value && typeof value === "object" && keys[i] in value) {
|
||||
value = value[keys[i]]
|
||||
} else {
|
||||
// Indicate this key does not even exists in the english fallback
|
||||
return `## ${key} ##`
|
||||
}
|
||||
}
|
||||
|
||||
// Make untranslated string easy to spot
|
||||
value = `<i>${value}</i>`
|
||||
} else if (notFound) {
|
||||
// No fallback available
|
||||
return `## ${key} ##`
|
||||
}
|
||||
|
||||
if (typeof value !== "string") {
|
||||
if (debug) {
|
||||
Logger.warn("I18n", `Translation key "${key}" is not a string`)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// Handle interpolations (e.g., "Hello {name}!")
|
||||
var result = value
|
||||
for (var placeholder in interpolations) {
|
||||
const regex = new RegExp(`\\{${placeholder}\\}`, 'g')
|
||||
result = result.replace(regex, interpolations[placeholder])
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Plural translation function
|
||||
function trp(key, count, defaultSingular, defaultPlural, interpolations) {
|
||||
if (typeof defaultSingular === "undefined")
|
||||
defaultSingular = ""
|
||||
if (typeof defaultPlural === "undefined")
|
||||
defaultPlural = ""
|
||||
if (typeof interpolations === "undefined")
|
||||
interpolations = {}
|
||||
|
||||
const pluralKey = count === 1 ? key : `${key}_plural`
|
||||
const defaultValue = count === 1 ? defaultSingular : defaultPlural
|
||||
|
||||
// Merge interpolations with count (QML doesn't support spread operator)
|
||||
var finalInterpolations = {
|
||||
"count": count
|
||||
}
|
||||
for (var prop in interpolations) {
|
||||
finalInterpolations[prop] = interpolations[prop]
|
||||
}
|
||||
|
||||
return tr(pluralKey, finalInterpolations)
|
||||
}
|
||||
}
|
||||
+51
-15
@@ -4,22 +4,41 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Commons.IconsSets
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Expose the font family name for easy access
|
||||
readonly property string fontFamily: fontLoader.name
|
||||
readonly property string fontFamily: currentFontLoader ? currentFontLoader.name : ""
|
||||
readonly property string defaultIcon: TablerIcons.defaultIcon
|
||||
readonly property var icons: TablerIcons.icons
|
||||
readonly property var aliases: TablerIcons.aliases
|
||||
readonly property string fontPath: "/Assets/Fonts/tabler/tabler-icons.ttf"
|
||||
|
||||
// Current active font loader
|
||||
property FontLoader currentFontLoader: null
|
||||
property int fontVersion: 0
|
||||
|
||||
// Create a unique cache-busting path
|
||||
readonly property string cacheBustingPath: Quickshell.shellDir + fontPath + "?v=" + fontVersion + "&t=" + Date.now()
|
||||
|
||||
// Signal emitted when font is reloaded
|
||||
signal fontReloaded
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.log("Icons", "Service started")
|
||||
loadFontWithCacheBusting()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Quickshell
|
||||
function onReloadCompleted() {
|
||||
Logger.log("Icons", "Quickshell reload completed - forcing font reload")
|
||||
reloadFont()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
function get(iconName) {
|
||||
// Check in aliases first
|
||||
if (aliases[iconName] !== undefined) {
|
||||
@@ -30,20 +49,37 @@ Singleton {
|
||||
return icons[iconName]
|
||||
}
|
||||
|
||||
FontLoader {
|
||||
id: fontLoader
|
||||
source: Quickshell.shellDir + fontPath
|
||||
function loadFontWithCacheBusting() {
|
||||
Logger.log("Icons", "Loading font with cache busting:", cacheBustingPath)
|
||||
|
||||
// Destroy old loader first
|
||||
if (currentFontLoader) {
|
||||
currentFontLoader.destroy()
|
||||
currentFontLoader = null
|
||||
}
|
||||
|
||||
// Create new loader with cache-busting URL
|
||||
currentFontLoader = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
FontLoader {
|
||||
source: "${cacheBustingPath}"
|
||||
}
|
||||
`, root, "dynamicFontLoader_" + fontVersion)
|
||||
|
||||
// Connect to the new loader's status changes
|
||||
currentFontLoader.statusChanged.connect(function () {
|
||||
if (currentFontLoader.status === FontLoader.Ready) {
|
||||
Logger.log("Icons", "Font loaded successfully:", currentFontLoader.name, "(version " + fontVersion + ")")
|
||||
fontReloaded()
|
||||
} else if (currentFontLoader.status === FontLoader.Error) {
|
||||
Logger.error("Icons", "Font failed to load (version " + fontVersion + ")")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Monitor font loading status
|
||||
Connections {
|
||||
target: fontLoader
|
||||
function onStatusChanged() {
|
||||
if (fontLoader.status === FontLoader.Ready) {
|
||||
Logger.log("Icons", "Font loaded successfully:", fontFamily)
|
||||
} else if (fontLoader.status === FontLoader.Error) {
|
||||
Logger.error("Icons", "Font failed to load")
|
||||
}
|
||||
}
|
||||
function reloadFont() {
|
||||
Logger.log("Icons", "Forcing font reload...")
|
||||
fontVersion++
|
||||
loadFontWithCacheBusting()
|
||||
}
|
||||
}
|
||||
|
||||
+417
-364
@@ -5,10 +5,16 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import "../Helpers/QtObj2JS.js" as QtObj2JS
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Used to access via Settings.data.xxx.yyy
|
||||
readonly property alias data: adapter
|
||||
property bool isLoaded: false
|
||||
property bool directoriesCreated: false
|
||||
|
||||
// Define our app directories
|
||||
// Default config directory: ~/.config/noctalia
|
||||
// Default cache directory: ~/.cache/noctalia
|
||||
@@ -16,187 +22,47 @@ Singleton {
|
||||
property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/" + shellName + "/"
|
||||
property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env("HOME") + "/.cache") + "/" + shellName + "/"
|
||||
property string cacheDirImages: cacheDir + "images/"
|
||||
|
||||
property string cacheDirImagesWallpapers: cacheDir + "images/wallpapers/"
|
||||
property string cacheDirImagesNotifications: cacheDir + "images/notifications/"
|
||||
property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json")
|
||||
|
||||
property string defaultLocation: "Tokyo"
|
||||
property string defaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png"
|
||||
|
||||
property string defaultAvatar: Quickshell.env("HOME") + "/.face"
|
||||
property string defaultVideosDirectory: Quickshell.env("HOME") + "/Videos"
|
||||
property string defaultLocation: "Tokyo"
|
||||
property string defaultWallpapersDirectory: Quickshell.env("HOME") + "/Pictures/Wallpapers"
|
||||
property string defaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png"
|
||||
|
||||
// Used to access via Settings.data.xxx.yyy
|
||||
readonly property alias data: adapter
|
||||
|
||||
property bool isLoaded: false
|
||||
property bool directoriesCreated: false
|
||||
|
||||
// Signal emitted when settings are loaded after startupcale changes
|
||||
signal settingsLoaded
|
||||
|
||||
// -----------------------------------------------------
|
||||
// Function to validate monitor configurations
|
||||
function validateMonitorConfigurations() {
|
||||
var availableScreenNames = []
|
||||
for (var i = 0; i < Quickshell.screens.length; i++) {
|
||||
availableScreenNames.push(Quickshell.screens[i].name)
|
||||
}
|
||||
|
||||
Logger.log("Settings", "Available monitors: [" + availableScreenNames.join(", ") + "]")
|
||||
Logger.log("Settings", "Configured bar monitors: [" + adapter.bar.monitors.join(", ") + "]")
|
||||
|
||||
// Check bar monitors
|
||||
if (adapter.bar.monitors.length > 0) {
|
||||
var hasValidBarMonitor = false
|
||||
for (var j = 0; j < adapter.bar.monitors.length; j++) {
|
||||
if (availableScreenNames.includes(adapter.bar.monitors[j])) {
|
||||
hasValidBarMonitor = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!hasValidBarMonitor) {
|
||||
Logger.warn("Settings", "No configured bar monitors found on system, clearing bar monitor list to show on all screens")
|
||||
adapter.bar.monitors = []
|
||||
} else {
|
||||
|
||||
//Logger.log("Settings", "Found valid bar monitors, keeping configuration")
|
||||
}
|
||||
} else {
|
||||
|
||||
//Logger.log("Settings", "Bar monitor list is empty, will show on all available screens")
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// If the settings structure has changed, ensure
|
||||
// backward compatibility by upgrading the settings
|
||||
function upgradeSettingsData() {
|
||||
|
||||
const sections = ["left", "center", "right"]
|
||||
|
||||
// -----------------
|
||||
// 1st. check our settings are not super old, when we only had the widget type as a plain string
|
||||
for (var s = 0; s < sections.length; s++) {
|
||||
const sectionName = sections[s]
|
||||
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
|
||||
var widget = adapter.bar.widgets[sectionName][i]
|
||||
if (typeof widget === "string") {
|
||||
adapter.bar.widgets[sectionName][i] = {
|
||||
"id": widget
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// 2nd. remove any non existing widget type
|
||||
for (var s = 0; s < sections.length; s++) {
|
||||
const sectionName = sections[s]
|
||||
const widgets = adapter.bar.widgets[sectionName]
|
||||
// Iterate backward through the widgets array, so it does not break when removing a widget
|
||||
for (var i = widgets.length - 1; i >= 0; i--) {
|
||||
var widget = widgets[i]
|
||||
if (!BarWidgetRegistry.hasWidget(widget.id)) {
|
||||
widgets.splice(i, 1)
|
||||
Logger.warn(`Settings`, `Deleted invalid widget ${widget.id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// 3nd. migrate global settings to user settings
|
||||
for (var s = 0; s < sections.length; s++) {
|
||||
const sectionName = sections[s]
|
||||
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
|
||||
var widget = adapter.bar.widgets[sectionName][i]
|
||||
|
||||
// Check if widget registry supports user settings, if it does not, then there is nothing to do
|
||||
const reg = BarWidgetRegistry.widgetMetadata[widget.id]
|
||||
if ((reg === undefined) || (reg.allowUserSettings === undefined) || !reg.allowUserSettings) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (upgradeWidget(widget)) {
|
||||
Logger.log("Settings", `Upgraded ${widget.id} widget:`, JSON.stringify(widget))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade the density of the bar so the look stay the same for people who upgrade.
|
||||
if (adapter.settingsVersion == 2) {
|
||||
adapter.bar.density = "comfortable"
|
||||
adapter.settingsVersion++
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
function upgradeWidget(widget) {
|
||||
// Backup the widget definition before altering
|
||||
const widgetBefore = JSON.stringify(widget)
|
||||
|
||||
switch (widget.id) {
|
||||
// Get back to global settings for these two clock settings
|
||||
case "Clock":
|
||||
if (widget.use12HourClock !== undefined) {
|
||||
adapter.location.use12hourFormat = widget.use12HourClock
|
||||
delete widget.use12HourClock
|
||||
}
|
||||
if (widget.reverseDayMonth !== undefined) {
|
||||
adapter.location.monthBeforeDay = widget.reverseDayMonth
|
||||
delete widget.reverseDayMonth
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Inject missing default setting (metaData) from BarWidgetRegistry
|
||||
const keys = Object.keys(BarWidgetRegistry.widgetMetadata[widget.id])
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
const k = keys[i]
|
||||
if (k === "id" || k === "allowUserSettings") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (widget[k] === undefined) {
|
||||
widget[k] = BarWidgetRegistry.widgetMetadata[widget.id][k]
|
||||
}
|
||||
}
|
||||
|
||||
// Compare settings, to detect if something has been upgraded
|
||||
const widgetAfter = JSON.stringify(widget)
|
||||
return (widgetAfter !== widgetBefore)
|
||||
}
|
||||
// -----------------------------------------------------
|
||||
// Kickoff essential services
|
||||
function kickOffServices() {
|
||||
// Ensure our location singleton is created as soon as possible so we start fetching weather asap
|
||||
LocationService.init()
|
||||
|
||||
NightLightService.apply()
|
||||
|
||||
ColorSchemeService.init()
|
||||
|
||||
MatugenService.init()
|
||||
|
||||
// Ensure wallpapers are restored after settings have been loaded
|
||||
WallpaperService.init()
|
||||
|
||||
FontService.init()
|
||||
|
||||
HooksService.init()
|
||||
|
||||
BluetoothService.init()
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// Ensure directories exist before FileView tries to read files
|
||||
Component.onCompleted: {
|
||||
// ensure settings dir exists
|
||||
Quickshell.execDetached(["mkdir", "-p", configDir])
|
||||
Quickshell.execDetached(["mkdir", "-p", cacheDir])
|
||||
Quickshell.execDetached(["mkdir", "-p", cacheDirImages])
|
||||
|
||||
Quickshell.execDetached(["mkdir", "-p", cacheDirImagesWallpapers])
|
||||
Quickshell.execDetached(["mkdir", "-p", cacheDirImagesNotifications])
|
||||
|
||||
// Mark directories as created and trigger file loading
|
||||
directoriesCreated = true
|
||||
|
||||
// This should only be activated once when the settings structure has changed
|
||||
// Then it should be commented out again, regular users don't need to generate
|
||||
// default settings on every start
|
||||
// TODO: automate this someday!
|
||||
//generateDefaultSettings()
|
||||
|
||||
// Patch-in the local default, resolved to user's home
|
||||
adapter.general.avatarImage = defaultAvatar
|
||||
adapter.screenRecorder.directory = defaultVideosDirectory
|
||||
adapter.wallpaper.directory = defaultWallpapersDirectory
|
||||
|
||||
// Set the adapter to the settingsFileView to trigger the real settings load
|
||||
settingsFileView.adapter = adapter
|
||||
}
|
||||
|
||||
// Don't write settings to disk immediately
|
||||
@@ -244,213 +110,400 @@ Singleton {
|
||||
// File doesn't exist, create it with default values
|
||||
writeAdapter()
|
||||
}
|
||||
}
|
||||
JsonAdapter {
|
||||
id: adapter
|
||||
|
||||
JsonAdapter {
|
||||
id: adapter
|
||||
property int settingsVersion: 6
|
||||
|
||||
property int settingsVersion: 3
|
||||
// bar
|
||||
property JsonObject bar: JsonObject {
|
||||
property string position: "top" // "top", "bottom", "left", or "right"
|
||||
property real backgroundOpacity: 1.0
|
||||
property list<string> monitors: []
|
||||
property string density: "default" // "compact", "default", "comfortable"
|
||||
property bool showCapsule: true
|
||||
|
||||
// bar
|
||||
property JsonObject bar: JsonObject {
|
||||
property string position: "top" // "top", "bottom", "left", or "right"
|
||||
property real backgroundOpacity: 1.0
|
||||
property list<string> monitors: []
|
||||
property string density: "default" // "compact", "default", "comfortable"
|
||||
property bool showCapsule: true
|
||||
// Floating bar settings
|
||||
property bool floating: false
|
||||
property real marginVertical: 0.25
|
||||
property real marginHorizontal: 0.25
|
||||
|
||||
// Floating bar settings
|
||||
property bool floating: false
|
||||
property real marginVertical: 0.25
|
||||
property real marginHorizontal: 0.25
|
||||
|
||||
// Widget configuration for modular bar system
|
||||
property JsonObject widgets
|
||||
widgets: JsonObject {
|
||||
property list<var> left: [{
|
||||
"id": "SystemMonitor"
|
||||
}, {
|
||||
"id": "ActiveWindow"
|
||||
}, {
|
||||
"id": "MediaMini"
|
||||
}]
|
||||
property list<var> center: [{
|
||||
"id": "Workspace"
|
||||
}]
|
||||
property list<var> right: [{
|
||||
"id": "ScreenRecorderIndicator"
|
||||
}, {
|
||||
"id": "Tray"
|
||||
}, {
|
||||
"id": "NotificationHistory"
|
||||
}, {
|
||||
"id": "WiFi"
|
||||
}, {
|
||||
"id": "Bluetooth"
|
||||
}, {
|
||||
"id": "Battery"
|
||||
}, {
|
||||
"id": "Volume"
|
||||
}, {
|
||||
"id": "Brightness"
|
||||
}, {
|
||||
"id": "NightLight"
|
||||
}, {
|
||||
"id": "Clock"
|
||||
}, {
|
||||
"id": "SidePanelToggle"
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
// general
|
||||
property JsonObject general: JsonObject {
|
||||
property string avatarImage: defaultAvatar
|
||||
property bool dimDesktop: true
|
||||
property bool showScreenCorners: false
|
||||
property bool forceBlackScreenCorners: false
|
||||
property real radiusRatio: 1.0
|
||||
property real screenRadiusRatio: 1.0
|
||||
property real animationSpeed: 1.0
|
||||
}
|
||||
|
||||
// location
|
||||
property JsonObject location: JsonObject {
|
||||
property string name: defaultLocation
|
||||
property bool useFahrenheit: false
|
||||
property bool use12hourFormat: false
|
||||
property bool monthBeforeDay: false
|
||||
property bool showWeekNumberInCalendar: false
|
||||
}
|
||||
|
||||
// screen recorder
|
||||
property JsonObject screenRecorder: JsonObject {
|
||||
property string directory: defaultVideosDirectory
|
||||
property int frameRate: 60
|
||||
property string audioCodec: "opus"
|
||||
property string videoCodec: "h264"
|
||||
property string quality: "very_high"
|
||||
property string colorRange: "limited"
|
||||
property bool showCursor: true
|
||||
property string audioSource: "default_output"
|
||||
property string videoSource: "portal"
|
||||
}
|
||||
|
||||
// wallpaper
|
||||
property JsonObject wallpaper: JsonObject {
|
||||
property bool enabled: true
|
||||
property string directory: defaultWallpapersDirectory
|
||||
property bool enableMultiMonitorDirectories: false
|
||||
property bool setWallpaperOnAllMonitors: true
|
||||
property string fillMode: "crop"
|
||||
property color fillColor: "#000000"
|
||||
property bool randomEnabled: false
|
||||
property int randomIntervalSec: 300 // 5 min
|
||||
property int transitionDuration: 1500 // 1500 ms
|
||||
property string transitionType: "random"
|
||||
property real transitionEdgeSmoothness: 0.05
|
||||
property list<var> monitors: []
|
||||
}
|
||||
|
||||
// applauncher
|
||||
property JsonObject appLauncher: JsonObject {
|
||||
property bool enableClipboardHistory: false
|
||||
// Position: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center
|
||||
property string position: "center"
|
||||
property real backgroundOpacity: 1.0
|
||||
property list<string> pinnedExecs: []
|
||||
property bool useApp2Unit: false
|
||||
property bool sortByMostUsed: true
|
||||
}
|
||||
|
||||
// dock
|
||||
property JsonObject dock: JsonObject {
|
||||
property bool autoHide: false
|
||||
property bool exclusive: false
|
||||
property real backgroundOpacity: 1.0
|
||||
property real floatingRatio: 1.0
|
||||
property list<string> monitors: []
|
||||
}
|
||||
|
||||
// network
|
||||
property JsonObject network: JsonObject {
|
||||
property bool wifiEnabled: true
|
||||
property bool bluetoothEnabled: true
|
||||
}
|
||||
|
||||
// notifications
|
||||
property JsonObject notifications: JsonObject {
|
||||
property bool doNotDisturb: false
|
||||
property list<string> monitors: []
|
||||
// Last time the user opened the notification history (ms since e899999999999998poch)
|
||||
property real lastSeenTs: 0
|
||||
// Duration settings for different urgency levels (in seconds)
|
||||
property int lowUrgencyDuration: 3
|
||||
property int normalUrgencyDuration: 8
|
||||
property int criticalUrgencyDuration: 15
|
||||
}
|
||||
|
||||
// audio
|
||||
property JsonObject audio: JsonObject {
|
||||
property int volumeStep: 5
|
||||
property int cavaFrameRate: 60
|
||||
property string visualizerType: "linear"
|
||||
property list<string> mprisBlacklist: []
|
||||
property string preferredPlayer: ""
|
||||
}
|
||||
|
||||
// ui
|
||||
property JsonObject ui: JsonObject {
|
||||
property string fontDefault: "Roboto"
|
||||
property string fontFixed: "DejaVu Sans Mono"
|
||||
property string fontBillboard: "Inter"
|
||||
property list<var> monitorsScaling: []
|
||||
property bool idleInhibitorEnabled: false
|
||||
}
|
||||
|
||||
// brightness
|
||||
property JsonObject brightness: JsonObject {
|
||||
property int brightnessStep: 5
|
||||
}
|
||||
|
||||
property JsonObject colorSchemes: JsonObject {
|
||||
property bool useWallpaperColors: false
|
||||
property string predefinedScheme: ""
|
||||
property bool darkMode: true
|
||||
}
|
||||
|
||||
// matugen templates toggles
|
||||
property JsonObject matugen: JsonObject {
|
||||
// Per-template flags to control dynamic config generation
|
||||
property bool gtk4: false
|
||||
property bool gtk3: false
|
||||
property bool qt6: false
|
||||
property bool qt5: false
|
||||
property bool kitty: false
|
||||
property bool ghostty: false
|
||||
property bool foot: false
|
||||
property bool fuzzel: false
|
||||
property bool vesktop: false
|
||||
property bool pywalfox: false
|
||||
property bool enableUserTemplates: false
|
||||
}
|
||||
|
||||
// night light
|
||||
property JsonObject nightLight: JsonObject {
|
||||
property bool enabled: false
|
||||
property bool forced: false
|
||||
property bool autoSchedule: true
|
||||
property string nightTemp: "4000"
|
||||
property string dayTemp: "6500"
|
||||
property string manualSunrise: "06:30"
|
||||
property string manualSunset: "18:30"
|
||||
}
|
||||
|
||||
// hooks
|
||||
property JsonObject hooks: JsonObject {
|
||||
property bool enabled: false
|
||||
property string wallpaperChange: ""
|
||||
property string darkModeChange: ""
|
||||
// Widget configuration for modular bar system
|
||||
property JsonObject widgets
|
||||
widgets: JsonObject {
|
||||
property list<var> left: [{
|
||||
"id": "SystemMonitor"
|
||||
}, {
|
||||
"id": "ActiveWindow"
|
||||
}, {
|
||||
"id": "MediaMini"
|
||||
}]
|
||||
property list<var> center: [{
|
||||
"id": "Workspace"
|
||||
}]
|
||||
property list<var> right: [{
|
||||
"id": "ScreenRecorder"
|
||||
}, {
|
||||
"id": "Tray"
|
||||
}, {
|
||||
"id": "NotificationHistory"
|
||||
}, {
|
||||
"id": "WiFi"
|
||||
}, {
|
||||
"id": "Bluetooth"
|
||||
}, {
|
||||
"id": "Battery"
|
||||
}, {
|
||||
"id": "Volume"
|
||||
}, {
|
||||
"id": "Brightness"
|
||||
}, {
|
||||
"id": "NightLight"
|
||||
}, {
|
||||
"id": "Clock"
|
||||
}, {
|
||||
"id": "ControlCenter"
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
// general
|
||||
property JsonObject general: JsonObject {
|
||||
property string avatarImage: ""
|
||||
property bool dimDesktop: true
|
||||
property bool showScreenCorners: false
|
||||
property bool forceBlackScreenCorners: false
|
||||
property real radiusRatio: 1.0
|
||||
property real screenRadiusRatio: 1.0
|
||||
property real animationSpeed: 1.0
|
||||
}
|
||||
|
||||
// location
|
||||
property JsonObject location: JsonObject {
|
||||
property string name: defaultLocation
|
||||
property bool useFahrenheit: false
|
||||
property bool use12hourFormat: false
|
||||
property bool showWeekNumberInCalendar: false
|
||||
}
|
||||
|
||||
// screen recorder
|
||||
property JsonObject screenRecorder: JsonObject {
|
||||
property string directory: ""
|
||||
property int frameRate: 60
|
||||
property string audioCodec: "opus"
|
||||
property string videoCodec: "h264"
|
||||
property string quality: "very_high"
|
||||
property string colorRange: "limited"
|
||||
property bool showCursor: true
|
||||
property string audioSource: "default_output"
|
||||
property string videoSource: "portal"
|
||||
}
|
||||
|
||||
// wallpaper
|
||||
property JsonObject wallpaper: JsonObject {
|
||||
property bool enabled: true
|
||||
property string directory: ""
|
||||
property bool enableMultiMonitorDirectories: false
|
||||
property bool setWallpaperOnAllMonitors: true
|
||||
property string fillMode: "crop"
|
||||
property color fillColor: "#000000"
|
||||
property bool randomEnabled: false
|
||||
property int randomIntervalSec: 300 // 5 min
|
||||
property int transitionDuration: 1500 // 1500 ms
|
||||
property string transitionType: "random"
|
||||
property real transitionEdgeSmoothness: 0.05
|
||||
property list<var> monitors: []
|
||||
}
|
||||
|
||||
// applauncher
|
||||
property JsonObject appLauncher: JsonObject {
|
||||
property bool enableClipboardHistory: false
|
||||
// Position: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center
|
||||
property string position: "center"
|
||||
property real backgroundOpacity: 1.0
|
||||
property list<string> pinnedExecs: []
|
||||
property bool useApp2Unit: false
|
||||
property bool sortByMostUsed: true
|
||||
}
|
||||
|
||||
// dock
|
||||
property JsonObject dock: JsonObject {
|
||||
property bool autoHide: false
|
||||
property bool exclusive: false
|
||||
property real backgroundOpacity: 1.0
|
||||
property real floatingRatio: 1.0
|
||||
property list<string> monitors: []
|
||||
// Desktop entry IDs pinned to the dock (e.g., "org.kde.konsole", "firefox.desktop")
|
||||
property list<string> pinnedApps: []
|
||||
}
|
||||
|
||||
// network
|
||||
property JsonObject network: JsonObject {
|
||||
property bool wifiEnabled: true
|
||||
property bool bluetoothEnabled: true
|
||||
}
|
||||
|
||||
// notifications
|
||||
property JsonObject notifications: JsonObject {
|
||||
property bool doNotDisturb: false
|
||||
property list<string> monitors: []
|
||||
property string location: "top_right"
|
||||
property bool alwaysOnTop: false
|
||||
property real lastSeenTs: 0
|
||||
property bool respectExpireTimeout: false
|
||||
property int lowUrgencyDuration: 3
|
||||
property int normalUrgencyDuration: 8
|
||||
property int criticalUrgencyDuration: 15
|
||||
property bool enableOSD: true
|
||||
}
|
||||
|
||||
// audio
|
||||
property JsonObject audio: JsonObject {
|
||||
property int volumeStep: 5
|
||||
property bool volumeOverdrive: false
|
||||
property int cavaFrameRate: 60
|
||||
property string visualizerType: "linear"
|
||||
property list<string> mprisBlacklist: []
|
||||
property string preferredPlayer: ""
|
||||
}
|
||||
|
||||
// ui
|
||||
property JsonObject ui: JsonObject {
|
||||
property string fontDefault: "Roboto"
|
||||
property string fontFixed: "DejaVu Sans Mono"
|
||||
property string fontBillboard: "Inter"
|
||||
property list<var> monitorsScaling: []
|
||||
property bool idleInhibitorEnabled: false
|
||||
}
|
||||
|
||||
// brightness
|
||||
property JsonObject brightness: JsonObject {
|
||||
property int brightnessStep: 5
|
||||
}
|
||||
|
||||
property JsonObject colorSchemes: JsonObject {
|
||||
property bool useWallpaperColors: false
|
||||
property string predefinedScheme: "Noctalia (default)"
|
||||
property bool darkMode: true
|
||||
}
|
||||
|
||||
// matugen templates toggles
|
||||
property JsonObject matugen: JsonObject {
|
||||
// Per-template flags to control dynamic config generation
|
||||
property bool gtk4: false
|
||||
property bool gtk3: false
|
||||
property bool qt6: false
|
||||
property bool qt5: false
|
||||
property bool kitty: false
|
||||
property bool ghostty: false
|
||||
property bool foot: false
|
||||
property bool fuzzel: false
|
||||
property bool vesktop: false
|
||||
property bool pywalfox: false
|
||||
property bool enableUserTemplates: false
|
||||
}
|
||||
|
||||
// night light
|
||||
property JsonObject nightLight: JsonObject {
|
||||
property bool enabled: false
|
||||
property bool forced: false
|
||||
property bool autoSchedule: true
|
||||
property string nightTemp: "4000"
|
||||
property string dayTemp: "6500"
|
||||
property string manualSunrise: "06:30"
|
||||
property string manualSunset: "18:30"
|
||||
}
|
||||
|
||||
// hooks
|
||||
property JsonObject hooks: JsonObject {
|
||||
property bool enabled: false
|
||||
property string wallpaperChange: ""
|
||||
property string darkModeChange: ""
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// Generate default settings at the root of the repo
|
||||
function generateDefaultSettings() {
|
||||
try {
|
||||
Logger.log("Settings", "Generating settings-default.json")
|
||||
|
||||
// Prepare a clean JSON
|
||||
var plainAdapter = QtObj2JS.qtObjectToPlainObject(adapter)
|
||||
var jsonData = JSON.stringify(plainAdapter, null, 2)
|
||||
|
||||
var defaultPath = Quickshell.shellDir + "/Assets/settings-default.json"
|
||||
|
||||
// Encode transfer it has base64 to avoid any escaping issue
|
||||
var base64Data = Qt.btoa(jsonData)
|
||||
Quickshell.execDetached(["sh", "-c", `echo "${base64Data}" | base64 -d > "${defaultPath}"`])
|
||||
} catch (error) {
|
||||
Logger.error("Settings", "Failed to generate default settings file: " + error)
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// Function to validate monitor configurations
|
||||
function validateMonitorConfigurations() {
|
||||
var availableScreenNames = []
|
||||
for (var i = 0; i < Quickshell.screens.length; i++) {
|
||||
availableScreenNames.push(Quickshell.screens[i].name)
|
||||
}
|
||||
|
||||
Logger.log("Settings", "Available monitors: [" + availableScreenNames.join(", ") + "]")
|
||||
Logger.log("Settings", "Configured bar monitors: [" + adapter.bar.monitors.join(", ") + "]")
|
||||
|
||||
// Check bar monitors
|
||||
if (adapter.bar.monitors.length > 0) {
|
||||
var hasValidBarMonitor = false
|
||||
for (var j = 0; j < adapter.bar.monitors.length; j++) {
|
||||
if (availableScreenNames.includes(adapter.bar.monitors[j])) {
|
||||
hasValidBarMonitor = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!hasValidBarMonitor) {
|
||||
Logger.warn("Settings", "No configured bar monitors found on system, clearing bar monitor list to show on all screens")
|
||||
adapter.bar.monitors = []
|
||||
} else {
|
||||
|
||||
//Logger.log("Settings", "Found valid bar monitors, keeping configuration")
|
||||
}
|
||||
} else {
|
||||
|
||||
//Logger.log("Settings", "Bar monitor list is empty, will show on all available screens")
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// If the settings structure has changed, ensure
|
||||
// backward compatibility by upgrading the settings
|
||||
function upgradeSettingsData() {
|
||||
|
||||
const sections = ["left", "center", "right"]
|
||||
|
||||
// -----------------
|
||||
// 1st. convert old widget id to new id
|
||||
for (var s = 0; s < sections.length; s++) {
|
||||
const sectionName = sections[s]
|
||||
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
|
||||
var widget = adapter.bar.widgets[sectionName][i]
|
||||
|
||||
switch (widget.id) {
|
||||
case "DarkModeToggle":
|
||||
widget.id = "DarkMode"
|
||||
break
|
||||
case "PowerToggle":
|
||||
widget.id = "SessionMenu"
|
||||
break
|
||||
case "ScreenRecorderIndicator":
|
||||
widget.id = "ScreenRecorder"
|
||||
break
|
||||
case "SidePanelToggle":
|
||||
widget.id = "ControlCenter"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// 2nd. remove any non existing widget type
|
||||
for (var s = 0; s < sections.length; s++) {
|
||||
const sectionName = sections[s]
|
||||
const widgets = adapter.bar.widgets[sectionName]
|
||||
// Iterate backward through the widgets array, so it does not break when removing a widget
|
||||
for (var i = widgets.length - 1; i >= 0; i--) {
|
||||
var widget = widgets[i]
|
||||
if (!BarWidgetRegistry.hasWidget(widget.id)) {
|
||||
Logger.warn(`Settings`, `Deleted invalid widget ${widget.id}`)
|
||||
widgets.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// 3nd. migrate global settings to user settings
|
||||
for (var s = 0; s < sections.length; s++) {
|
||||
const sectionName = sections[s]
|
||||
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
|
||||
var widget = adapter.bar.widgets[sectionName][i]
|
||||
|
||||
// Check if widget registry supports user settings, if it does not, then there is nothing to do
|
||||
const reg = BarWidgetRegistry.widgetMetadata[widget.id]
|
||||
if ((reg === undefined) || (reg.allowUserSettings === undefined) || !reg.allowUserSettings) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (upgradeWidget(widget)) {
|
||||
Logger.log("Settings", `Upgraded ${widget.id} widget:`, JSON.stringify(widget))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade the density of the bar so the look stay the same for people who upgrade.
|
||||
// TODO: remove soon
|
||||
if (adapter.settingsVersion == 2) {
|
||||
adapter.bar.density = "comfortable"
|
||||
adapter.settingsVersion++
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
function upgradeWidget(widget) {
|
||||
// Backup the widget definition before altering
|
||||
const widgetBefore = JSON.stringify(widget)
|
||||
|
||||
switch (widget.id) {
|
||||
// Get back to global settings for these two clock settings
|
||||
case "Clock":
|
||||
if (widget.use12HourClock !== undefined) {
|
||||
adapter.location.use12hourFormat = widget.use12HourClock
|
||||
delete widget.use12HourClock
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Get all existing custom settings keys
|
||||
const keys = Object.keys(BarWidgetRegistry.widgetMetadata[widget.id])
|
||||
|
||||
// Delete deprecated user settings from the wiget
|
||||
for (const k of Object.keys(widget)) {
|
||||
if (k === "id" || k === "allowUserSettings") {
|
||||
continue
|
||||
}
|
||||
if (!keys.includes(k)) {
|
||||
delete widget[k]
|
||||
}
|
||||
}
|
||||
|
||||
// Inject missing default setting (metaData) from BarWidgetRegistry
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
const k = keys[i]
|
||||
if (k === "id" || k === "allowUserSettings") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (widget[k] === undefined) {
|
||||
widget[k] = BarWidgetRegistry.widgetMetadata[widget.id][k]
|
||||
}
|
||||
}
|
||||
|
||||
// Compare settings, to detect if something has been upgraded
|
||||
const widgetAfter = JSON.stringify(widget)
|
||||
return (widgetAfter !== widgetBefore)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// Kickoff essential services
|
||||
function kickOffServices() {
|
||||
LocationService.init()
|
||||
NightLightService.apply()
|
||||
ColorSchemeService.init()
|
||||
MatugenService.init()
|
||||
WallpaperService.init()
|
||||
FontService.init()
|
||||
HooksService.init()
|
||||
BluetoothService.init()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@ Singleton {
|
||||
"keep-awake-on": "mug",
|
||||
"keep-awake-off": "mug-off",
|
||||
"disc": "disc-filled",
|
||||
"eye": "eye",
|
||||
"pin": "pin",
|
||||
"unpin": "pinned-off",
|
||||
"image": "photo",
|
||||
"dark-mode": "contrast-filled",
|
||||
"camera-video": "video",
|
||||
@@ -3471,6 +3474,7 @@ Singleton {
|
||||
"http-que-off": "\u{100df}",
|
||||
"http-trace": "\u{fa30}",
|
||||
"http-trace-off": "\u{100de}",
|
||||
"hyprland": "\u{ec6a}",
|
||||
"ice-cream": "\u{eac2}",
|
||||
"ice-cream-2": "\u{ee9f}",
|
||||
"ice-cream-off": "\u{f148}",
|
||||
@@ -4303,6 +4307,7 @@ Singleton {
|
||||
"news-off": "\u{f167}",
|
||||
"nfc": "\u{eeb7}",
|
||||
"nfc-off": "\u{f168}",
|
||||
"niri": "\u{ec32}",
|
||||
"noctalia": "\u{ec33}",
|
||||
"no-copyright": "\u{efb9}",
|
||||
"no-creative-commons": "\u{efba}",
|
||||
+58
-74
@@ -8,6 +8,7 @@ import qs.Services
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Current date
|
||||
property var date: new Date()
|
||||
|
||||
// Returns a Unix Timestamp (in seconds)
|
||||
@@ -15,90 +16,73 @@ Singleton {
|
||||
return Math.floor(date / 1000)
|
||||
}
|
||||
|
||||
function formatDate(monthBeforeDay = true) {
|
||||
let now = date
|
||||
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
|
||||
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
|
||||
let day = now.getDate()
|
||||
let suffix
|
||||
if (day > 3 && day < 21)
|
||||
suffix = 'th'
|
||||
else
|
||||
switch (day % 10) {
|
||||
case 1:
|
||||
suffix = "st"
|
||||
break
|
||||
case 2:
|
||||
suffix = "nd"
|
||||
break
|
||||
case 3:
|
||||
suffix = "rd"
|
||||
break
|
||||
default:
|
||||
suffix = "th"
|
||||
}
|
||||
let month = now.toLocaleDateString(Qt.locale(), "MMMM")
|
||||
let year = now.toLocaleDateString(Qt.locale(), "yyyy")
|
||||
|
||||
return `${dayName}, ` + (monthBeforeDay ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`)
|
||||
Timer {
|
||||
interval: 1000
|
||||
repeat: true
|
||||
running: true
|
||||
onTriggered: root.date = new Date()
|
||||
}
|
||||
|
||||
// Formats a Date object into a YYYYMMDD-HHMMSS string.
|
||||
function getFormattedTimestamp(date) {
|
||||
if (!date) {
|
||||
date = new Date()
|
||||
}
|
||||
const year = date.getFullYear()
|
||||
|
||||
/**
|
||||
* Formats a Date object into a YYYYMMDD-HHMMSS string.
|
||||
* @param {Date} [date=new Date()] - The date to format. Defaults to the current date and time.
|
||||
* @returns {string} The formatted date string.
|
||||
*/
|
||||
function getFormattedTimestamp(date = new Date()) {
|
||||
const year = date.getFullYear()
|
||||
// getMonth() is zero-based, so we add 1
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
// getMonth() is zero-based, so we add 1
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
|
||||
return `${year}${month}${day}-${hours}${minutes}${seconds}`
|
||||
}
|
||||
|
||||
// Format an easy to read approximate duration ex: 4h32m
|
||||
// Used to display the time remaining on the Battery widget, computer uptime, etc..
|
||||
function formatVagueHumanReadableDuration(totalSeconds) {
|
||||
if (typeof totalSeconds !== 'number' || totalSeconds < 0) {
|
||||
return '0s'
|
||||
return `${year}${month}${day}-${hours}${minutes}${seconds}`
|
||||
}
|
||||
|
||||
// Floor the input to handle decimal seconds
|
||||
totalSeconds = Math.floor(totalSeconds)
|
||||
// Format an easy to read approximate duration ex: 4h32m
|
||||
// Used to display the time remaining on the Battery widget, computer uptime, etc..
|
||||
function formatVagueHumanReadableDuration(totalSeconds) {
|
||||
if (typeof totalSeconds !== 'number' || totalSeconds < 0) {
|
||||
return '0s'
|
||||
}
|
||||
|
||||
const days = Math.floor(totalSeconds / 86400)
|
||||
const hours = Math.floor((totalSeconds % 86400) / 3600)
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
const seconds = totalSeconds % 60
|
||||
// Floor the input to handle decimal seconds
|
||||
totalSeconds = Math.floor(totalSeconds)
|
||||
|
||||
const parts = []
|
||||
if (days)
|
||||
parts.push(`${days}d`)
|
||||
if (hours)
|
||||
parts.push(`${hours}h`)
|
||||
if (minutes)
|
||||
parts.push(`${minutes}m`)
|
||||
const days = Math.floor(totalSeconds / 86400)
|
||||
const hours = Math.floor((totalSeconds % 86400) / 3600)
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
const seconds = totalSeconds % 60
|
||||
|
||||
// Only show seconds if no hours and no minutes
|
||||
if (!hours && !minutes) {
|
||||
parts.push(`${seconds}s`)
|
||||
const parts = []
|
||||
if (days)
|
||||
parts.push(`${days}d`)
|
||||
if (hours)
|
||||
parts.push(`${hours}h`)
|
||||
if (minutes)
|
||||
parts.push(`${minutes}m`)
|
||||
|
||||
// Only show seconds if no hours and no minutes
|
||||
if (!hours && !minutes) {
|
||||
parts.push(`${seconds}s`)
|
||||
}
|
||||
|
||||
return parts.join('')
|
||||
}
|
||||
|
||||
return parts.join('')
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
repeat: true
|
||||
running: true
|
||||
|
||||
onTriggered: root.date = new Date()
|
||||
}
|
||||
// Format a date into
|
||||
function formatRelativeTime(date) {
|
||||
if (!date)
|
||||
return ""
|
||||
const diff = Date.now() - date.getTime()
|
||||
if (diff < 60000)
|
||||
return "now"
|
||||
if (diff < 3600000)
|
||||
return `${Math.floor(diff / 60000)}m ago`
|
||||
if (diff < 86400000)
|
||||
return `${Math.floor(diff / 3600000)}h ago`
|
||||
return `${Math.floor(diff / 86400000)}d ago`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
// -----------------------------------------------------
|
||||
// Helper function to convert Qt objects to plain JavaScript objects
|
||||
// Only used when generating settings-default.json
|
||||
function qtObjectToPlainObject(obj) {
|
||||
if (obj === null || obj === undefined) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Handle primitive types
|
||||
if (typeof obj !== "object") {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Handle native JavaScript arrays
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => qtObjectToPlainObject(item));
|
||||
}
|
||||
|
||||
// Detect QML arrays FIRST (before color detection)
|
||||
// QML arrays have a numeric length property and indexed properties
|
||||
if (typeof obj.length === "number" && obj.length >= 0) {
|
||||
// Check if it has indexed properties - be more flexible about detection
|
||||
var hasIndexedProps = true;
|
||||
var hasNumericKeys = false;
|
||||
|
||||
// Check if we have at least some numeric properties
|
||||
for (var i = 0; i < obj.length; i++) {
|
||||
if (obj.hasOwnProperty(i) || obj[i] !== undefined) {
|
||||
hasNumericKeys = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have length > 0 and some numeric keys, treat as array
|
||||
if (obj.length > 0 && hasNumericKeys) {
|
||||
var arr = [];
|
||||
for (var i = 0; i < obj.length; i++) {
|
||||
// Use direct property access, handle undefined gracefully
|
||||
var item = obj[i];
|
||||
if (item !== undefined) {
|
||||
arr.push(qtObjectToPlainObject(item));
|
||||
}
|
||||
}
|
||||
return arr; // Return here to avoid processing as object
|
||||
}
|
||||
|
||||
// Handle empty arrays (length = 0)
|
||||
if (obj.length === 0) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Detect and convert QML color objects to hex strings
|
||||
if (
|
||||
typeof obj.r === "number" &&
|
||||
typeof obj.g === "number" &&
|
||||
typeof obj.b === "number" &&
|
||||
typeof obj.a === "number" &&
|
||||
typeof obj.valid === "boolean"
|
||||
) {
|
||||
// This looks like a QML color object
|
||||
try {
|
||||
// Try to get the string representation (should be hex like "#000000")
|
||||
if (typeof obj.toString === "function") {
|
||||
return obj.toString();
|
||||
} else {
|
||||
// Fallback: convert RGBA to hex manually
|
||||
var r = Math.round(obj.r * 255);
|
||||
var g = Math.round(obj.g * 255);
|
||||
var b = Math.round(obj.b * 255);
|
||||
var hex =
|
||||
"#" +
|
||||
r.toString(16).padStart(2, "0") +
|
||||
g.toString(16).padStart(2, "0") +
|
||||
b.toString(16).padStart(2, "0");
|
||||
return hex;
|
||||
}
|
||||
} catch (e) {
|
||||
// If conversion fails, fall through to regular object handling
|
||||
}
|
||||
}
|
||||
|
||||
// Handle regular objects
|
||||
var plainObj = {};
|
||||
|
||||
// Get all property names, but filter out Qt-specific ones
|
||||
var propertyNames = Object.getOwnPropertyNames(obj);
|
||||
|
||||
for (var i = 0; i < propertyNames.length; i++) {
|
||||
var propName = propertyNames[i];
|
||||
|
||||
// Skip Qt-specific properties, functions, and array-like properties
|
||||
if (
|
||||
propName === "objectName" ||
|
||||
propName === "objectNameChanged" ||
|
||||
propName === "length" || // Skip length property
|
||||
/^\d+$/.test(propName) || // Skip numeric keys (0, 1, 2, etc.)
|
||||
propName.endsWith("Changed") ||
|
||||
typeof obj[propName] === "function"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
var value = obj[propName];
|
||||
plainObj[propName] = qtObjectToPlainObject(value);
|
||||
} catch (e) {
|
||||
// Skip properties that can't be accessed
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return plainObj;
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Widgets
|
||||
|
||||
Variants {
|
||||
|
||||
+12
-43
@@ -28,7 +28,7 @@ Variants {
|
||||
}
|
||||
}
|
||||
|
||||
active: Settings.isLoaded && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false
|
||||
active: Settings.isLoaded && BarService.isVisible && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
screen: modelData || null
|
||||
@@ -47,45 +47,14 @@ Variants {
|
||||
}
|
||||
|
||||
// Floating bar margins - only apply when floating is enabled
|
||||
// Also don't apply margin on the opposite side ot the bar orientation, ex: if bar is floating on top, margin is only applied on top, not bottom.
|
||||
margins {
|
||||
top: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
|
||||
bottom: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
|
||||
left: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
|
||||
right: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
|
||||
top: Settings.data.bar.floating && Settings.data.bar.position !== "bottom" ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
|
||||
bottom: Settings.data.bar.floating && Settings.data.bar.position !== "top" ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
|
||||
left: Settings.data.bar.floating && Settings.data.bar.position !== "right" ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
|
||||
right: Settings.data.bar.floating && Settings.data.bar.position !== "left" ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
|
||||
}
|
||||
|
||||
|
||||
// Filter out potentially invisible widgets so they we don't have extra spacing around an invisble widget.
|
||||
// TODO: add activewindow
|
||||
property var visibleLeftWidgets: {
|
||||
if (!Settings.data.bar.widgets.left) return []
|
||||
return Settings.data.bar.widgets.left.filter(widget => {
|
||||
if (widget.id === "ScreenRecorderIndicator") {
|
||||
return ScreenRecorderService.isRecording
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
property var visibleCenterWidgets: {
|
||||
if (!Settings.data.bar.widgets.center) return []
|
||||
return Settings.data.bar.widgets.center.filter(widget => {
|
||||
if (widget.id === "ScreenRecorderIndicator") {
|
||||
return ScreenRecorderService.isRecording
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
property var visibleRightWidgets: {
|
||||
if (!Settings.data.bar.widgets.right) return []
|
||||
return Settings.data.bar.widgets.right.filter(widget => {
|
||||
if (widget.id === "ScreenRecorderIndicator") {
|
||||
return ScreenRecorderService.isRecording
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
@@ -120,7 +89,7 @@ Variants {
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: visibleLeftWidgets
|
||||
model: Settings.data.bar.widgets.left
|
||||
delegate: BarWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
@@ -143,7 +112,7 @@ Variants {
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: visibleCenterWidgets
|
||||
model: Settings.data.bar.widgets.center
|
||||
delegate: BarWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
@@ -167,7 +136,7 @@ Variants {
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: visibleRightWidgets
|
||||
model: Settings.data.bar.widgets.right
|
||||
delegate: BarWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
@@ -201,7 +170,7 @@ Variants {
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: visibleLeftWidgets
|
||||
model: Settings.data.bar.widgets.left
|
||||
delegate: BarWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
@@ -226,7 +195,7 @@ Variants {
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: visibleCenterWidgets
|
||||
model: Settings.data.bar.widgets.center
|
||||
delegate: BarWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
@@ -252,7 +221,7 @@ Variants {
|
||||
spacing: Style.marginS * root.scaling
|
||||
|
||||
Repeater {
|
||||
model: visibleRightWidgets
|
||||
model: Settings.data.bar.widgets.right
|
||||
delegate: BarWidgetLoader {
|
||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||
widgetProps: {
|
||||
|
||||
-40
@@ -176,46 +176,6 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MouseArea {
|
||||
|
||||
// id: availableDeviceArea
|
||||
// acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
// anchors.fill: parent
|
||||
// hoverEnabled: true
|
||||
// cursorShape: (canConnect || canDisconnect)
|
||||
// && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
|
||||
// onEntered: {
|
||||
// if (root.tooltipText && !isBusy) {
|
||||
// tooltip.show()
|
||||
// }
|
||||
// }
|
||||
// onExited: {
|
||||
// if (root.tooltipText && !isBusy) {
|
||||
// tooltip.hide()
|
||||
// }
|
||||
// }
|
||||
// onClicked: function (mouse) {
|
||||
|
||||
// if (!modelData || modelData.pairing) {
|
||||
// return
|
||||
// }
|
||||
|
||||
// if (root.tooltipText && !isBusy) {
|
||||
// tooltip.hide()
|
||||
// }
|
||||
|
||||
// if (mouse.button === Qt.LeftButton) {
|
||||
// if (modelData.connected) {
|
||||
// BluetoothService.disconnectDevice(modelData)
|
||||
// } else {
|
||||
// BluetoothService.connectDeviceWithTrust(modelData)
|
||||
// }
|
||||
// } else if (mouse.button === Qt.RightButton) {
|
||||
// BluetoothService.forgetDevice(modelData)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-11
@@ -35,7 +35,7 @@ NPanel {
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Bluetooth"
|
||||
text: I18n.tr("bluetooth.panel.title")
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
@@ -52,7 +52,7 @@ NPanel {
|
||||
NIconButton {
|
||||
enabled: Settings.data.network.bluetoothEnabled
|
||||
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "refresh"
|
||||
tooltipText: "Refresh Devices"
|
||||
tooltipText: I18n.tr("tooltips.refresh-devices")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
if (BluetoothService.adapter) {
|
||||
@@ -63,7 +63,7 @@ NPanel {
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close."
|
||||
tooltipText: I18n.tr("tooltips.close")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
root.close()
|
||||
@@ -94,14 +94,14 @@ NPanel {
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Bluetooth is disabled"
|
||||
text: I18n.tr("bluetooth.panel.disabled")
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Enable Bluetooth to see available devices."
|
||||
text: I18n.tr("bluetooth.panel.enable-message")
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
@@ -124,7 +124,7 @@ NPanel {
|
||||
|
||||
// Connected devices
|
||||
BluetoothDevicesList {
|
||||
label: "Connected devices"
|
||||
label: I18n.tr("bluetooth.panel.connected-devices")
|
||||
property var items: {
|
||||
if (!BluetoothService.adapter || !Bluetooth.devices)
|
||||
return []
|
||||
@@ -138,8 +138,8 @@ NPanel {
|
||||
|
||||
// Known devices
|
||||
BluetoothDevicesList {
|
||||
label: "Known devices"
|
||||
tooltipText: "Left click to connect.\nRight click to forget."
|
||||
label: I18n.tr("bluetooth.panel.known-devices")
|
||||
tooltipText: I18n.tr("tooltips.connect-disconnect-devices")
|
||||
property var items: {
|
||||
if (!BluetoothService.adapter || !Bluetooth.devices)
|
||||
return []
|
||||
@@ -153,7 +153,7 @@ NPanel {
|
||||
|
||||
// Available devices
|
||||
BluetoothDevicesList {
|
||||
label: "Available devices"
|
||||
label: I18n.tr("bluetooth.panel.available-devices")
|
||||
property var items: {
|
||||
if (!BluetoothService.adapter || !Bluetooth.devices)
|
||||
return []
|
||||
@@ -199,14 +199,14 @@ NPanel {
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Scanning for devices..."
|
||||
text: I18n.tr("bluetooth.panel.scanning")
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Make sure your device is in pairing mode."
|
||||
text: I18n.tr("bluetooth.panel.pairing-mode")
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
@@ -10,15 +10,18 @@ import qs.Widgets
|
||||
NPanel {
|
||||
id: root
|
||||
|
||||
preferredWidth: Settings.data.location.showWeekNumberInCalendar ? 350 : 330
|
||||
preferredHeight: 320
|
||||
preferredWidth: Settings.data.location.showWeekNumberInCalendar ? 320 : 300
|
||||
preferredHeight: 300
|
||||
|
||||
// Main Column
|
||||
panelContent: ColumnLayout {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
readonly property int firstDayOfWeek: Qt.locale().firstDayOfWeek
|
||||
|
||||
// Header: Month/Year with navigation
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
@@ -28,7 +31,7 @@ NPanel {
|
||||
|
||||
NIconButton {
|
||||
icon: "chevron-left"
|
||||
tooltipText: "Previous month"
|
||||
tooltipText: I18n.tr("tooltips.previous-month")
|
||||
onClicked: {
|
||||
let newDate = new Date(grid.year, grid.month - 1, 1)
|
||||
grid.year = newDate.getFullYear()
|
||||
@@ -47,7 +50,7 @@ NPanel {
|
||||
|
||||
NIconButton {
|
||||
icon: "chevron-right"
|
||||
tooltipText: "Next month"
|
||||
tooltipText: I18n.tr("tooltips.next-month")
|
||||
onClicked: {
|
||||
let newDate = new Date(grid.year, grid.month + 1, 1)
|
||||
grid.year = newDate.getFullYear()
|
||||
@@ -78,7 +81,7 @@ NPanel {
|
||||
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
text: "Week"
|
||||
text: I18n.tr("calendar.panel.week")
|
||||
color: Color.mOutline
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
font.weight: Style.fontWeightRegular
|
||||
@@ -106,9 +109,7 @@ NPanel {
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
// Use the locale's first day of week setting
|
||||
let firstDay = Qt.locale().firstDayOfWeek
|
||||
let dayIndex = (firstDay + index) % 7
|
||||
let dayIndex = (content.firstDayOfWeek + index) % 7
|
||||
return Qt.locale().dayName(dayIndex, Locale.ShortFormat)
|
||||
}
|
||||
color: Color.mSecondary
|
||||
@@ -130,22 +131,19 @@ NPanel {
|
||||
spacing: 0
|
||||
|
||||
// Week numbers column (only visible when enabled)
|
||||
GridLayout {
|
||||
ColumnLayout {
|
||||
visible: Settings.data.location.showWeekNumberInCalendar
|
||||
Layout.preferredWidth: visible ? Style.baseWidgetSize * scaling : 0
|
||||
Layout.fillHeight: true
|
||||
columns: 1
|
||||
rows: 6
|
||||
columnSpacing: 0
|
||||
rowSpacing: 0
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: 6 // Maximum 6 weeks in a month view
|
||||
|
||||
Rectangle {
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Color.transparent
|
||||
Layout.preferredHeight: Style.baseWidgetSize * scaling
|
||||
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
@@ -153,30 +151,54 @@ NPanel {
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
text: {
|
||||
// Calculate the first day shown in the calendar grid
|
||||
let firstDay = new Date(grid.year, grid.month, 1)
|
||||
let firstDayOfWeek = Qt.locale().firstDayOfWeek
|
||||
let startOffset = (firstDay.getDay() - firstDayOfWeek + 7) % 7
|
||||
let gridStartDate = new Date(grid.year, grid.month, 1 - startOffset)
|
||||
// Calculate the date shown in the first column of this row
|
||||
// MonthGrid always shows 42 days (6 weeks × 7 days)
|
||||
|
||||
// Get the date for the start of this specific row
|
||||
let rowDate = new Date(gridStartDate)
|
||||
rowDate.setDate(gridStartDate.getDate() + (index * 7))
|
||||
// First, find the first day of the month
|
||||
let firstOfMonth = new Date(grid.year, grid.month, 1)
|
||||
|
||||
// Calculate week number based on the Thursday of the visual row
|
||||
// This correctly handles rows that span two different ISO weeks.
|
||||
let thursdayOfRow = new Date(rowDate)
|
||||
let offsetToThursday = (4 - thursdayOfRow.getDay() + 7) % 7
|
||||
thursdayOfRow.setDate(thursdayOfRow.getDate() + offsetToThursday)
|
||||
// Calculate how many days before the 1st to start the grid
|
||||
// This depends on the locale's first day of week
|
||||
let firstDayOfWeek = content.firstDayOfWeek
|
||||
let firstOfMonthDayOfWeek = firstOfMonth.getDay()
|
||||
|
||||
// Check if this row is visible (contains days from current month)
|
||||
let rowEndDate = new Date(rowDate)
|
||||
rowEndDate.setDate(rowDate.getDate() + 6)
|
||||
// Calculate offset: how many days before the 1st should the grid start?
|
||||
let daysBeforeFirst = (firstOfMonthDayOfWeek - firstDayOfWeek + 7) % 7
|
||||
|
||||
if (rowDate.getMonth() === grid.month || rowEndDate.getMonth() === grid.month || (rowDate.getMonth() < grid.month && rowEndDate.getMonth() > grid.month)) {
|
||||
return `${getISOWeekNumber(thursdayOfRow)}`
|
||||
// MonthGrid typically shows the previous month's days to fill the first week
|
||||
// If the 1st is already on the first day of week, show the previous week
|
||||
if (daysBeforeFirst === 0) {
|
||||
daysBeforeFirst = 7
|
||||
}
|
||||
return ""
|
||||
|
||||
// Calculate the start date of the grid
|
||||
let gridStartDate = new Date(grid.year, grid.month, 1 - daysBeforeFirst)
|
||||
|
||||
// Calculate the date for this specific row (week)
|
||||
let rowStartDate = new Date(gridStartDate)
|
||||
rowStartDate.setDate(gridStartDate.getDate() + (index * 7))
|
||||
|
||||
// For ISO week numbers, we need to find the Thursday of this week
|
||||
// ISO 8601 week numbering: week with year's first Thursday is week 1
|
||||
// The week number is determined by the Thursday
|
||||
|
||||
// Find the Thursday of this row's week
|
||||
// If firstDayOfWeek is Monday (1), Thursday is +3 days
|
||||
// If firstDayOfWeek is Sunday (0), we need to adjust
|
||||
let thursday = new Date(rowStartDate)
|
||||
if (firstDayOfWeek === 0) {
|
||||
// Sunday start: Thursday is 4 days after Sunday
|
||||
thursday.setDate(rowStartDate.getDate() + 4)
|
||||
} else if (firstDayOfWeek === 1) {
|
||||
// Monday start: Thursday is 3 days after Monday
|
||||
thursday.setDate(rowStartDate.getDate() + 3)
|
||||
} else {
|
||||
// Other start days: calculate offset to Thursday
|
||||
let daysToThursday = (4 - firstDayOfWeek + 7) % 7
|
||||
thursday.setDate(rowStartDate.getDate() + daysToThursday)
|
||||
}
|
||||
|
||||
return `${getISOWeekNumber(thursday)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,24 +216,27 @@ NPanel {
|
||||
year: Time.date.getFullYear()
|
||||
locale: Qt.locale()
|
||||
|
||||
delegate: Rectangle {
|
||||
width: Style.baseWidgetSize * scaling
|
||||
height: Style.baseWidgetSize * scaling
|
||||
radius: Style.radiusS * scaling
|
||||
color: model.today ? Color.mPrimary : Color.transparent
|
||||
delegate: Item {
|
||||
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
text: model.day
|
||||
color: model.today ? Color.mOnPrimary : Color.mOnSurface
|
||||
opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular
|
||||
}
|
||||
Rectangle {
|
||||
width: Style.baseWidgetSize * scaling
|
||||
height: Style.baseWidgetSize * scaling
|
||||
radius: width / 2
|
||||
color: model.today ? Color.mPrimary : Color.transparent
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
text: model.day
|
||||
color: model.today ? Color.mOnPrimary : Color.mOnSurface
|
||||
opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,21 +244,27 @@ NPanel {
|
||||
}
|
||||
}
|
||||
|
||||
// ISO 8601 week number calculation
|
||||
// This is locale-independent and always uses Monday as first day of week
|
||||
function getISOWeekNumber(date) {
|
||||
// Create a copy of the date and normalize to noon to prevent DST issues
|
||||
const targetDate = new Date(date.getTime())
|
||||
targetDate.setHours(12, 0, 0, 0)
|
||||
// Create a copy and set to nearest Thursday (current date + 4 - current day number)
|
||||
// ISO week starts on Monday (1) to Sunday (7)
|
||||
const target = new Date(date.getTime())
|
||||
target.setHours(0, 0, 0, 0)
|
||||
|
||||
// Roll the date to the Thursday of the week.
|
||||
// getDay() is 0 for Sunday, we want Monday to be 1 and Sunday to be 7.
|
||||
const dayOfWeek = targetDate.getDay() || 7
|
||||
targetDate.setDate(targetDate.getDate() - dayOfWeek + 4)
|
||||
// Get day of week where Monday = 1, Sunday = 7
|
||||
const dayOfWeek = target.getDay() || 7
|
||||
|
||||
// Get the first day of that Thursday's year
|
||||
const yearStart = new Date(targetDate.getFullYear(), 0, 1)
|
||||
// Set to nearest Thursday (which determines the week number)
|
||||
target.setDate(target.getDate() + 4 - dayOfWeek)
|
||||
|
||||
// Calculate the difference in days and find the week number
|
||||
const dayOfYear = ((targetDate - yearStart) / 86400000) + 1
|
||||
return Math.ceil(dayOfYear / 7)
|
||||
// Get first day of year
|
||||
const yearStart = new Date(target.getFullYear(), 0, 1)
|
||||
|
||||
// Calculate full weeks between yearStart and target
|
||||
// Add 1 because we're counting weeks, not week differences
|
||||
const weekNumber = Math.ceil(((target - yearStart) / 86400000 + 1) / 7)
|
||||
|
||||
return weekNumber
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ Item {
|
||||
property bool compact: false
|
||||
|
||||
// Effective shown state (true if hovered/animated open or forced)
|
||||
readonly property bool revealed: forceOpen || showPill
|
||||
readonly property bool revealed: !forceClose && (forceOpen || showPill)
|
||||
|
||||
signal shown
|
||||
signal hidden
|
||||
@@ -221,7 +221,7 @@ Item {
|
||||
hovered = true
|
||||
root.entered()
|
||||
tooltip.show()
|
||||
if (disableOpen) {
|
||||
if (disableOpen || forceClose) {
|
||||
return
|
||||
}
|
||||
if (!forceOpen) {
|
||||
@@ -231,7 +231,7 @@ Item {
|
||||
onExited: {
|
||||
hovered = false
|
||||
root.exited()
|
||||
if (!forceOpen) {
|
||||
if (!forceOpen && !forceClose) {
|
||||
hide()
|
||||
}
|
||||
tooltip.hide()
|
||||
|
||||
@@ -40,7 +40,7 @@ NPanel {
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Wi-Fi"
|
||||
text: I18n.tr("wifi.panel.title")
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
@@ -56,7 +56,7 @@ NPanel {
|
||||
|
||||
NIconButton {
|
||||
icon: "refresh"
|
||||
tooltipText: "Refresh"
|
||||
tooltipText: I18n.tr("tooltips.refresh")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
enabled: Settings.data.network.wifiEnabled && !NetworkService.scanning
|
||||
onClicked: NetworkService.scan()
|
||||
@@ -64,7 +64,7 @@ NPanel {
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close."
|
||||
tooltipText: I18n.tr("tooltips.close")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: root.close()
|
||||
}
|
||||
@@ -136,14 +136,14 @@ NPanel {
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Wi-Fi is disabled"
|
||||
text: I18n.tr("wifi.panel.disabled")
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Enable Wi-Fi to see available networks."
|
||||
text: I18n.tr("wifi.panel.enable-message")
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
@@ -172,7 +172,7 @@ NPanel {
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Searching for nearby networks..."
|
||||
text: I18n.tr("wifi.panel.searching")
|
||||
font.pointSize: Style.fontSizeNormal * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
@@ -263,7 +263,9 @@ NPanel {
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
NText {
|
||||
text: `${modelData.signal}%`
|
||||
text: I18n.tr("system.signal-strength", {
|
||||
"signal": modelData.signal
|
||||
})
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
@@ -295,7 +297,7 @@ NPanel {
|
||||
NText {
|
||||
id: connectedText
|
||||
anchors.centerIn: parent
|
||||
text: "Connected"
|
||||
text: I18n.tr("wifi.panel.connected")
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
@@ -311,7 +313,7 @@ NPanel {
|
||||
NText {
|
||||
id: disconnectingText
|
||||
anchors.centerIn: parent
|
||||
text: "Disconnecting..."
|
||||
text: I18n.tr("wifi.panel.disconnecting")
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
@@ -327,7 +329,7 @@ NPanel {
|
||||
NText {
|
||||
id: forgettingText
|
||||
anchors.centerIn: parent
|
||||
text: "Forgetting..."
|
||||
text: I18n.tr("wifi.panel.forgetting")
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
@@ -345,7 +347,7 @@ NPanel {
|
||||
NText {
|
||||
id: savedText
|
||||
anchors.centerIn: parent
|
||||
text: "Saved"
|
||||
text: I18n.tr("wifi.panel.saved")
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
@@ -367,7 +369,7 @@ NPanel {
|
||||
NIconButton {
|
||||
visible: (modelData.existing || modelData.cached) && !modelData.connected && NetworkService.connectingTo !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
|
||||
icon: "trash"
|
||||
tooltipText: "Forget network"
|
||||
tooltipText: I18n.tr("tooltips.forget-network")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: expandedSsid = expandedSsid === modelData.ssid ? "" : modelData.ssid
|
||||
}
|
||||
@@ -376,10 +378,10 @@ NPanel {
|
||||
visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid && passwordSsid !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
|
||||
text: {
|
||||
if (modelData.existing || modelData.cached)
|
||||
return "Connect"
|
||||
return I18n.tr("wifi.panel.connect")
|
||||
if (!NetworkService.isSecured(modelData.security))
|
||||
return "Connect"
|
||||
return "Password"
|
||||
return I18n.tr("wifi.panel.connect")
|
||||
return I18n.tr("wifi.panel.password")
|
||||
}
|
||||
outlined: !hovered
|
||||
fontSize: Style.fontSizeXS * scaling
|
||||
@@ -397,7 +399,7 @@ NPanel {
|
||||
|
||||
NButton {
|
||||
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
|
||||
text: "Disconnect"
|
||||
text: I18n.tr("wifi.panel.disconnect")
|
||||
outlined: !hovered
|
||||
fontSize: Style.fontSizeXS * scaling
|
||||
backgroundColor: Color.mError
|
||||
@@ -457,7 +459,7 @@ NPanel {
|
||||
Text {
|
||||
visible: parent.text.length === 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Enter password..."
|
||||
text: I18n.tr("wifi.panel.enter-password")
|
||||
color: Color.mOnSurfaceVariant
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
}
|
||||
@@ -465,7 +467,7 @@ NPanel {
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: "Connect"
|
||||
text: I18n.tr("wifi.panel.connect")
|
||||
fontSize: Style.fontSizeXXS * scaling
|
||||
enabled: passwordInput.length > 0 && !NetworkService.connecting
|
||||
outlined: true
|
||||
@@ -511,7 +513,7 @@ NPanel {
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Forget this network?"
|
||||
text: I18n.tr("wifi.panel.forget-network")
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mError
|
||||
Layout.fillWidth: true
|
||||
@@ -520,7 +522,7 @@ NPanel {
|
||||
|
||||
NButton {
|
||||
id: forgetButton
|
||||
text: "Forget"
|
||||
text: I18n.tr("wifi.panel.forget")
|
||||
fontSize: Style.fontSizeXXS * scaling
|
||||
backgroundColor: Color.mError
|
||||
outlined: forgetButton.hovered ? false : true
|
||||
@@ -561,14 +563,14 @@ NPanel {
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "No networks found"
|
||||
text: I18n.tr("wifi.panel.no-networks")
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: "Scan again"
|
||||
text: I18n.tr("wifi.panel.scan-again")
|
||||
icon: "refresh"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: NetworkService.scan()
|
||||
@@ -30,60 +30,34 @@ Item {
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon
|
||||
|
||||
// 6% of total width
|
||||
readonly property real minWidth: Math.max(1, screen.width * 0.06)
|
||||
readonly property real maxWidth: minWidth * 2
|
||||
readonly property bool hasActiveWindow: CompositorService.getFocusedWindowTitle() !== ""
|
||||
readonly property string windowTitle: CompositorService.getFocusedWindowTitle() || "No active window"
|
||||
readonly property string fallbackIcon: "user-desktop"
|
||||
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property bool compact: (Settings.data.bar.density === "compact")
|
||||
|
||||
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
|
||||
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * 0.8 * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
|
||||
// Widget settings - matching MediaMini pattern
|
||||
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon
|
||||
readonly property bool autoHide: (widgetSettings.autoHide !== undefined) ? widgetSettings.autoHide : widgetMetadata.autoHide
|
||||
readonly property string scrollingMode: (widgetSettings.scrollingMode !== undefined) ? widgetSettings.scrollingMode : (widgetMetadata.scrollingMode !== undefined ? widgetMetadata.scrollingMode : "hover")
|
||||
|
||||
readonly property real textSize: {
|
||||
var base = isVertical ? width : height
|
||||
return Math.max(1, compact ? base * 0.43 : base * 0.33)
|
||||
}
|
||||
// Fixed width
|
||||
readonly property real widgetWidth: Math.max(1, screen.width * 0.06)
|
||||
|
||||
readonly property real iconSize: textSize * 1.25
|
||||
implicitHeight: visible ? ((barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)) : 0
|
||||
implicitWidth: visible ? ((barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (widgetWidth * scaling)) : 0
|
||||
|
||||
function getTitle() {
|
||||
try {
|
||||
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
|
||||
} catch (e) {
|
||||
Logger.warn("ActiveWindow", "Error getting title:", e)
|
||||
return ""
|
||||
opacity: !autoHide || hasActiveWindow ? 1.0 : 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
visible: getTitle() !== ""
|
||||
|
||||
function calculatedVerticalHeight() {
|
||||
// Use standard widget height like other widgets
|
||||
return Math.round(Style.capsuleHeight * scaling)
|
||||
}
|
||||
|
||||
function calculatedHorizontalWidth() {
|
||||
let total = Style.marginM * 2 * scaling // internal padding
|
||||
|
||||
if (showIcon) {
|
||||
total += Style.capsuleHeight * 0.5 * scaling + 2 * scaling // icon + spacing
|
||||
}
|
||||
|
||||
// Calculate actual text width more accurately
|
||||
const title = getTitle()
|
||||
if (title !== "") {
|
||||
// Estimate text width: average character width * number of characters
|
||||
const avgCharWidth = Style.fontSizeS * scaling * 0.6 // rough estimate
|
||||
const titleWidth = Math.min(title.length * avgCharWidth, 80 * scaling)
|
||||
total += titleWidth
|
||||
}
|
||||
|
||||
// Row layout handles spacing between widgets
|
||||
return Math.max(total, Style.capsuleHeight * scaling) // Minimum width
|
||||
return Math.round(Style.baseWidgetSize * 0.8 * scaling)
|
||||
}
|
||||
|
||||
function getAppIcon() {
|
||||
@@ -94,7 +68,7 @@ Item {
|
||||
try {
|
||||
const idValue = focusedWindow.appId
|
||||
const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue)
|
||||
const iconResult = AppIcons.iconForAppId(normalizedId.toLowerCase())
|
||||
const iconResult = ThemeIcons.iconForAppId(normalizedId.toLowerCase())
|
||||
if (iconResult && iconResult !== "") {
|
||||
return iconResult
|
||||
}
|
||||
@@ -103,49 +77,49 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to ToplevelManager
|
||||
if (ToplevelManager && ToplevelManager.activeToplevel) {
|
||||
try {
|
||||
const activeToplevel = ToplevelManager.activeToplevel
|
||||
if (activeToplevel.appId) {
|
||||
const idValue2 = activeToplevel.appId
|
||||
const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2)
|
||||
const iconResult2 = AppIcons.iconForAppId(normalizedId2.toLowerCase())
|
||||
if (iconResult2 && iconResult2 !== "") {
|
||||
return iconResult2
|
||||
if (CompositorService.isHyprland) {
|
||||
// Fallback to ToplevelManager
|
||||
if (ToplevelManager && ToplevelManager.activeToplevel) {
|
||||
try {
|
||||
const activeToplevel = ToplevelManager.activeToplevel
|
||||
if (activeToplevel.appId) {
|
||||
const idValue2 = activeToplevel.appId
|
||||
const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2)
|
||||
const iconResult2 = ThemeIcons.iconForAppId(normalizedId2.toLowerCase())
|
||||
if (iconResult2 && iconResult2 !== "") {
|
||||
return iconResult2
|
||||
}
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
Logger.warn("ActiveWindow", "Error getting icon from ToplevelManager:", fallbackError)
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
Logger.warn("ActiveWindow", "Error getting icon from ToplevelManager:", fallbackError)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
return ThemeIcons.iconFromName(fallbackIcon)
|
||||
} catch (e) {
|
||||
Logger.warn("ActiveWindow", "Error in getAppIcon:", e)
|
||||
return ""
|
||||
return ThemeIcons.iconFromName(fallbackIcon)
|
||||
}
|
||||
}
|
||||
|
||||
// A hidden text element to safely measure the full title width
|
||||
// Hidden text element to measure full title width
|
||||
NText {
|
||||
id: fullTitleMetrics
|
||||
visible: false
|
||||
text: getTitle()
|
||||
text: windowTitle
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: windowTitleRect
|
||||
id: windowActiveRect
|
||||
visible: root.visible
|
||||
anchors.left: (barPosition === "top" || barPosition === "bottom") ? parent.left : undefined
|
||||
anchors.top: (barPosition === "left" || barPosition === "right") ? parent.top : undefined
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
|
||||
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : Math.round(Style.capsuleHeight * scaling)
|
||||
radius: width / 2
|
||||
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (widgetWidth * scaling)
|
||||
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : Math.round(Style.capsuleHeight * scaling)
|
||||
radius: (barPosition === "left" || barPosition === "right") ? width / 2 : Math.round(Style.radiusM * scaling)
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
Item {
|
||||
@@ -153,21 +127,21 @@ Item {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
|
||||
anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
|
||||
clip: true
|
||||
|
||||
// Horizontal layout for top/bottom bars
|
||||
RowLayout {
|
||||
id: horizontalLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 2 * scaling
|
||||
id: rowLayout
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
visible: barPosition === "top" || barPosition === "bottom"
|
||||
z: 1
|
||||
|
||||
// Window icon
|
||||
Item {
|
||||
Layout.preferredWidth: Style.capsuleHeight * 0.75 * scaling
|
||||
Layout.preferredHeight: Style.capsuleHeight * 0.75 * scaling
|
||||
Layout.preferredWidth: Math.round(18 * scaling)
|
||||
Layout.preferredHeight: Math.round(18 * scaling)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: getTitle() !== "" && showIcon
|
||||
visible: showIcon
|
||||
|
||||
IconImage {
|
||||
id: windowIcon
|
||||
@@ -176,39 +150,139 @@ Item {
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
visible: source !== ""
|
||||
|
||||
// Handle loading errors gracefully
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error) {
|
||||
Logger.warn("ActiveWindow", "Failed to load icon:", source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
id: titleText
|
||||
// Title container with scrolling
|
||||
Item {
|
||||
id: titleContainer
|
||||
Layout.preferredWidth: {
|
||||
try {
|
||||
if (mouseArea.containsMouse) {
|
||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
|
||||
} else {
|
||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, 80 * scaling)) // Limited width for horizontal bars
|
||||
// Calculate available width based on other elements
|
||||
var iconWidth = (showIcon && windowIcon.visible ? (18 * scaling + Style.marginS * scaling) : 0)
|
||||
var totalMargins = Style.marginXXS * scaling * 2
|
||||
var availableWidth = mainContainer.width - iconWidth - totalMargins
|
||||
return Math.max(20 * scaling, availableWidth)
|
||||
}
|
||||
Layout.maximumWidth: Layout.preferredWidth
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredHeight: titleText.height
|
||||
|
||||
clip: true
|
||||
|
||||
property bool isScrolling: false
|
||||
property bool isResetting: false
|
||||
property real textWidth: fullTitleMetrics.contentWidth
|
||||
property real containerWidth: width
|
||||
property bool needsScrolling: textWidth > containerWidth
|
||||
|
||||
// Timer for "always" mode with delay
|
||||
Timer {
|
||||
id: scrollStartTimer
|
||||
interval: 1000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (scrollingMode === "always" && titleContainer.needsScrolling) {
|
||||
titleContainer.isScrolling = true
|
||||
titleContainer.isResetting = false
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.warn("ActiveWindow", "Error calculating width:", e)
|
||||
return 80 * scaling
|
||||
}
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: getTitle()
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
clip: true
|
||||
|
||||
// Update scrolling state based on mode
|
||||
property var updateScrollingState: function () {
|
||||
if (scrollingMode === "never") {
|
||||
isScrolling = false
|
||||
isResetting = false
|
||||
} else if (scrollingMode === "always") {
|
||||
if (needsScrolling) {
|
||||
if (mouseArea.containsMouse) {
|
||||
isScrolling = false
|
||||
isResetting = true
|
||||
} else {
|
||||
scrollStartTimer.restart()
|
||||
}
|
||||
} else {
|
||||
scrollStartTimer.stop()
|
||||
isScrolling = false
|
||||
isResetting = false
|
||||
}
|
||||
} else if (scrollingMode === "hover") {
|
||||
if (mouseArea.containsMouse && needsScrolling) {
|
||||
isScrolling = true
|
||||
isResetting = false
|
||||
} else {
|
||||
isScrolling = false
|
||||
if (needsScrolling) {
|
||||
isResetting = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onWidthChanged: updateScrollingState()
|
||||
Component.onCompleted: updateScrollingState()
|
||||
|
||||
// React to hover changes
|
||||
Connections {
|
||||
target: mouseArea
|
||||
function onContainsMouseChanged() {
|
||||
titleContainer.updateScrollingState()
|
||||
}
|
||||
}
|
||||
|
||||
// Scrolling content with seamless loop
|
||||
Item {
|
||||
id: scrollContainer
|
||||
height: parent.height
|
||||
width: childrenRect.width
|
||||
|
||||
property real scrollX: 0
|
||||
x: scrollX
|
||||
|
||||
Row {
|
||||
spacing: 50 * scaling // Gap between text copies
|
||||
|
||||
NText {
|
||||
id: titleText
|
||||
text: windowTitle
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
// Second copy for seamless scrolling
|
||||
NText {
|
||||
text: windowTitle
|
||||
font: titleText.font
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mOnSurface
|
||||
visible: titleContainer.needsScrolling && titleContainer.isScrolling
|
||||
}
|
||||
}
|
||||
|
||||
// Reset animation
|
||||
NumberAnimation on scrollX {
|
||||
running: titleContainer.isResetting
|
||||
to: 0
|
||||
duration: 300
|
||||
easing.type: Easing.OutQuad
|
||||
onFinished: {
|
||||
titleContainer.isResetting = false
|
||||
}
|
||||
}
|
||||
|
||||
// Seamless infinite scroll
|
||||
NumberAnimation on scrollX {
|
||||
id: infiniteScroll
|
||||
running: titleContainer.isScrolling && !titleContainer.isResetting
|
||||
from: 0
|
||||
to: -(titleContainer.textWidth + 50 * scaling)
|
||||
duration: Math.max(4000, windowTitle.length * 100)
|
||||
loops: Animation.Infinite
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
NumberAnimation {
|
||||
@@ -223,15 +297,17 @@ Item {
|
||||
Item {
|
||||
id: verticalLayout
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Style.marginXS * scaling * 2
|
||||
height: parent.height - Style.marginXS * scaling * 2
|
||||
width: parent.width - Style.marginM * scaling * 2
|
||||
height: parent.height - Style.marginM * scaling * 2
|
||||
visible: barPosition === "left" || barPosition === "right"
|
||||
z: 1
|
||||
|
||||
// Window icon
|
||||
Item {
|
||||
width: Style.capsuleHeight * 0.75 * scaling
|
||||
height: Style.capsuleHeight * 0.75 * scaling
|
||||
width: Style.baseWidgetSize * 0.5 * scaling
|
||||
height: Style.baseWidgetSize * 0.5 * scaling
|
||||
anchors.centerIn: parent
|
||||
visible: windowTitle !== ""
|
||||
|
||||
IconImage {
|
||||
id: windowIconVertical
|
||||
@@ -240,13 +316,6 @@ Item {
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
visible: source !== ""
|
||||
|
||||
// Handle loading errors gracefully
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error) {
|
||||
Logger.warn("ActiveWindow", "Failed to load icon:", source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,30 +326,31 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onEntered: {
|
||||
if (barPosition === "left" || barPosition === "right") {
|
||||
tooltip.show()
|
||||
} else if ((tooltip.text !== "") && (scrollingMode === "never")) {
|
||||
tooltip.show()
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (barPosition === "left" || barPosition === "right") {
|
||||
tooltip.hide()
|
||||
}
|
||||
tooltip.hide()
|
||||
}
|
||||
}
|
||||
|
||||
// Hover tooltip with full title (only for vertical bars)
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
target: verticalLayout
|
||||
text: getTitle()
|
||||
positionLeft: barPosition === "right"
|
||||
positionRight: barPosition === "left"
|
||||
delay: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
text: windowTitle
|
||||
target: (barPosition === "left" || barPosition === "right") ? verticalLayout : windowActiveRect
|
||||
positionLeft: barPosition === "right"
|
||||
positionRight: barPosition === "left"
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
delay: Style.tooltipDelay
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onActiveWindowChanged() {
|
||||
|
||||
@@ -54,7 +54,9 @@ Item {
|
||||
// Only notify once we are a below threshold
|
||||
if (!charging && !root.hasNotifiedLowBattery && percent <= warningThreshold) {
|
||||
root.hasNotifiedLowBattery = true
|
||||
ToastService.showWarning("Low Battery", `Battery is at ${Math.round(percent)}%. Please connect the charger.`)
|
||||
ToastService.showWarning(I18n.tr("toast.battery.low"), I18n.tr("toast.battery.low-desc", {
|
||||
"percent": Math.round(percent)
|
||||
}))
|
||||
} else if (root.hasNotifiedLowBattery && (charging || percent > warningThreshold + 5)) {
|
||||
// Reset when charging starts or when battery recovers 5% above threshold
|
||||
root.hasNotifiedLowBattery = false
|
||||
|
||||
@@ -21,7 +21,7 @@ NIconButton {
|
||||
colorBorderHover: Color.transparent
|
||||
|
||||
icon: Settings.data.network.bluetoothEnabled ? "bluetooth" : "bluetooth-off"
|
||||
tooltipText: "Bluetooth devices."
|
||||
tooltipText: I18n.tr("tooltips.bluetooth-devices")
|
||||
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
|
||||
onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Modules.Settings
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Bar.Extras
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
+54
-143
@@ -29,175 +29,86 @@ Rectangle {
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property bool compact: (Settings.data.bar.density === "compact")
|
||||
readonly property bool use12h: Settings.data.location.use12hourFormat
|
||||
readonly property bool monthBeforeDay: Settings.data.location.monthBeforeDay
|
||||
|
||||
readonly property var now: Time.date
|
||||
|
||||
// Resolve settings: try user settings or defaults from BarWidgetRegistry
|
||||
readonly property string displayFormat: widgetSettings.displayFormat !== undefined ? widgetSettings.displayFormat : widgetMetadata.displayFormat
|
||||
readonly property bool usePrimaryColor: widgetSettings.usePrimaryColor !== undefined ? widgetSettings.usePrimaryColor : widgetMetadata.usePrimaryColor
|
||||
property bool useMonospacedFont: widgetSettings.useMonospacedFont !== undefined ? widgetSettings.useMonospacedFont : widgetMetadata.useMonospacedFont
|
||||
readonly property string formatHorizontal: widgetSettings.formatHorizontal !== undefined ? widgetSettings.formatHorizontal : widgetMetadata.formatHorizontal
|
||||
readonly property string formatVertical: widgetSettings.formatVertical !== undefined ? widgetSettings.formatVertical : widgetMetadata.formatVertical
|
||||
|
||||
// Use compact mode for vertical bars
|
||||
readonly property bool verticalMode: barPosition === "left" || barPosition === "right"
|
||||
implicitWidth: isBarVertical ? Math.round(Style.capsuleHeight * scaling) : Math.round((isBarVertical ? verticalLoader.implicitWidth : horizontalLoader.implicitWidth) + Style.marginM * 2 * scaling)
|
||||
|
||||
implicitWidth: verticalMode ? Math.round(Style.capsuleHeight * scaling) : Math.round(layout.implicitWidth + Style.marginM * 2 * scaling)
|
||||
implicitHeight: verticalMode ? Math.round(Style.capsuleHeight * 2.5 * scaling) : Math.round(Style.capsuleHeight * scaling) // Match BarPill
|
||||
implicitHeight: isBarVertical ? Math.round(verticalLoader.implicitHeight + Style.marginS * 2 * scaling) : Math.round(Style.capsuleHeight * scaling)
|
||||
|
||||
radius: Math.round(Style.radiusS * scaling)
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
Item {
|
||||
id: clockContainer
|
||||
anchors.fill: parent
|
||||
anchors.margins: compact ? 0 : Style.marginXS * scaling
|
||||
anchors.centerIn: parent
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
// Horizontal
|
||||
Loader {
|
||||
id: horizontalLoader
|
||||
active: !isBarVertical
|
||||
anchors.centerIn: parent
|
||||
spacing: verticalMode ? -2 * scaling : -3 * scaling
|
||||
|
||||
// Compact mode for vertical bars - Time section (HH, MM)
|
||||
Repeater {
|
||||
model: verticalMode ? 2 : 1
|
||||
NText {
|
||||
readonly property bool showSeconds: (displayFormat === "time-seconds")
|
||||
readonly property bool inlineDate: (displayFormat === "time-date")
|
||||
readonly property var now: Time.date
|
||||
|
||||
text: {
|
||||
if (verticalMode) {
|
||||
// Compact mode: time section (first 2 lines)
|
||||
switch (index) {
|
||||
case 0:
|
||||
// Hours
|
||||
if (use12h) {
|
||||
const hours = now.getHours()
|
||||
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
|
||||
return displayHours.toString().padStart(2, '0')
|
||||
} else {
|
||||
return now.getHours().toString().padStart(2, '0')
|
||||
}
|
||||
case 1:
|
||||
// Minutes
|
||||
return now.getMinutes().toString().padStart(2, '0')
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
} else {
|
||||
// Normal mode: single line with time
|
||||
let timeStr = ""
|
||||
|
||||
if (use12h) {
|
||||
// 12-hour format with proper padding and consistent spacing
|
||||
const hours = now.getHours()
|
||||
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
|
||||
const paddedHours = displayHours.toString().padStart(2, '0')
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0')
|
||||
const ampm = hours < 12 ? 'AM' : 'PM'
|
||||
|
||||
if (showSeconds) {
|
||||
const seconds = now.getSeconds().toString().padStart(2, '0')
|
||||
timeStr = `${paddedHours}:${minutes}:${seconds} ${ampm}`
|
||||
} else {
|
||||
timeStr = `${paddedHours}:${minutes} ${ampm}`
|
||||
}
|
||||
sourceComponent: ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: Settings.data.bar.showCapsule ? -4 * scaling : -2 * scaling
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: Qt.formatDateTime(now, formatHorizontal.trim()).split("\\n")
|
||||
NText {
|
||||
visible: text !== ""
|
||||
text: modelData
|
||||
font.family: useMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault
|
||||
font.pointSize: {
|
||||
if (repeater.model.length == 1) {
|
||||
return Style.fontSizeS * scaling
|
||||
} else {
|
||||
// 24-hour format with padding
|
||||
const hours = now.getHours().toString().padStart(2, '0')
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0')
|
||||
|
||||
if (showSeconds) {
|
||||
const seconds = now.getSeconds().toString().padStart(2, '0')
|
||||
timeStr = `${hours}:${minutes}:${seconds}`
|
||||
} else {
|
||||
timeStr = `${hours}:${minutes}`
|
||||
}
|
||||
}
|
||||
|
||||
// Add inline date if needed
|
||||
if (inlineDate) {
|
||||
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
|
||||
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
|
||||
const day = now.getDate().toString().padStart(2, '0')
|
||||
let month = now.toLocaleDateString(Qt.locale(), "MMM")
|
||||
timeStr += " - " + (monthBeforeDay ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
|
||||
}
|
||||
|
||||
return timeStr
|
||||
}
|
||||
}
|
||||
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: verticalMode ? Style.fontSizeXXS * scaling : Style.fontSizeXS * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Separator line for compact mode (between time and date)
|
||||
Rectangle {
|
||||
visible: verticalMode
|
||||
Layout.preferredWidth: 20 * scaling
|
||||
Layout.preferredHeight: 2 * scaling
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 3 * scaling
|
||||
Layout.bottomMargin: 3 * scaling
|
||||
color: Color.mPrimary
|
||||
opacity: 0.3
|
||||
radius: 1 * scaling
|
||||
}
|
||||
|
||||
// Compact mode for vertical bars - Date section (DD, MM)
|
||||
Repeater {
|
||||
model: verticalMode ? 2 : 0
|
||||
NText {
|
||||
readonly property var now: Time.date
|
||||
|
||||
text: {
|
||||
if (verticalMode) {
|
||||
// Compact mode: date section (last 2 lines)
|
||||
switch (index) {
|
||||
case 0:
|
||||
return monthBeforeDay ? (now.getMonth() + 1).toString().padStart(2, '0') : now.getDate().toString().padStart(2, '0')
|
||||
case 1:
|
||||
return monthBeforeDay ? now.getDate().toString().padStart(2, '0') : (now.getMonth() + 1).toString().padStart(2, '0')
|
||||
default:
|
||||
return ""
|
||||
return (index == 0) ? Style.fontSizeXS * scaling : Style.fontSizeXXS * scaling
|
||||
}
|
||||
}
|
||||
return ""
|
||||
font.weight: Style.fontWeightBold
|
||||
color: usePrimaryColor ? Color.mPrimary : Color.mOnSurface
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
}
|
||||
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second line for normal mode (date)
|
||||
NText {
|
||||
visible: !verticalMode && (displayFormat === "time-date-short")
|
||||
text: {
|
||||
const now = Time.date
|
||||
const day = now.getDate().toString().padStart(2, '0')
|
||||
const month = (now.getMonth() + 1).toString().padStart(2, '0')
|
||||
return monthBeforeDay ? `${month}/${day}` : `${day}/${month}`
|
||||
// Vertical
|
||||
Loader {
|
||||
id: verticalLoader
|
||||
active: isBarVertical
|
||||
anchors.centerIn: parent // Now this works without layout conflicts
|
||||
sourceComponent: ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: -2 * scaling
|
||||
Repeater {
|
||||
model: Qt.formatDateTime(now, formatVertical.trim()).split(" ")
|
||||
delegate: NText {
|
||||
visible: text !== ""
|
||||
text: modelData
|
||||
font.family: useMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: usePrimaryColor ? Color.mPrimary : Color.mOnSurface
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Enable fixed-width font for consistent spacing
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
font.weight: Style.fontWeightRegular
|
||||
color: Color.mPrimary
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
text: `${Time.formatDate(monthBeforeDay)}.`
|
||||
text: I18n.tr("clock.tooltip")
|
||||
target: clockContainer
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
}
|
||||
|
||||
@@ -29,10 +29,13 @@ NIconButton {
|
||||
return {}
|
||||
}
|
||||
|
||||
readonly property string customIcon: widgetSettings.icon || widgetMetadata.icon
|
||||
readonly property bool useDistroLogo: (widgetSettings.useDistroLogo !== undefined) ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo
|
||||
readonly property string customIconPath: widgetSettings.customIconPath || ""
|
||||
|
||||
icon: useDistroLogo ? "" : "noctalia"
|
||||
tooltipText: "Open side panel."
|
||||
// If we have a custom path or distro logo, don't use the theme icon.
|
||||
icon: (customIconPath === "" && !useDistroLogo) ? customIcon : ""
|
||||
tooltipText: I18n.tr("tooltips.open-control-center")
|
||||
baseSize: Style.capsuleHeight
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
@@ -40,16 +43,22 @@ NIconButton {
|
||||
colorBgHover: useDistroLogo ? Color.mSurfaceVariant : Color.mTertiary
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: useDistroLogo ? Color.mTertiary : Color.transparent
|
||||
onClicked: PanelService.getPanel("sidePanel")?.toggle(this)
|
||||
onClicked: PanelService.getPanel("controlCenterPanel")?.toggle(this)
|
||||
onRightClicked: PanelService.getPanel("settingsPanel")?.toggle()
|
||||
|
||||
IconImage {
|
||||
id: logo
|
||||
id: customOrDistroLogo
|
||||
anchors.centerIn: parent
|
||||
width: root.width * 0.8
|
||||
height: width
|
||||
source: useDistroLogo ? DistroLogoService.osLogo : ""
|
||||
visible: useDistroLogo && source !== ""
|
||||
source: {
|
||||
if (customIconPath !== "")
|
||||
return customIconPath
|
||||
if (useDistroLogo)
|
||||
return DistroLogoService.osLogo
|
||||
return ""
|
||||
}
|
||||
visible: source !== ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Modules.Settings
|
||||
import qs.Modules.Bar.Extras
|
||||
|
||||
Item {
|
||||
@@ -57,7 +57,7 @@ Item {
|
||||
disableOpen: true
|
||||
tooltipText: {
|
||||
if (!hasExec) {
|
||||
return "Custom Button - Configure in settings"
|
||||
return "Custom button, configure in settings."
|
||||
} else {
|
||||
var lines = []
|
||||
if (leftClickExec !== "") {
|
||||
|
||||
@@ -10,7 +10,7 @@ NIconButton {
|
||||
property real scaling: 1.0
|
||||
|
||||
icon: "dark-mode"
|
||||
tooltipText: "Toggle light/dark mode."
|
||||
tooltipText: Settings.data.colorSchemes.darkMode ? I18n.tr("tooltips.switch-to-light-mode") : I18n.tr("tooltips.switch-to-dark-mode")
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
baseSize: Style.capsuleHeight
|
||||
colorBg: Settings.data.colorSchemes.darkMode ? (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) : Color.mPrimary
|
||||
@@ -14,9 +14,10 @@ NIconButton {
|
||||
baseSize: Style.capsuleHeight
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
|
||||
tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake" : "Enable keep awake"
|
||||
tooltipText: IdleInhibitorService.isInhibited ? I18n.tr("tooltips.disable-keep-awake") : I18n.tr("tooltips.enable-keep-awake")
|
||||
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: IdleInhibitorService.manualToggle()
|
||||
}
|
||||
|
||||
@@ -48,13 +48,14 @@ Item {
|
||||
icon: "keyboard"
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: currentLayout.toUpperCase()
|
||||
tooltipText: "Keyboard layout: " + currentLayout.toUpperCase()
|
||||
tooltipText: I18n.tr("tooltips.keyboard-layout", {
|
||||
"layout": currentLayout.toUpperCase()
|
||||
})
|
||||
forceOpen: root.displayMode === "forceOpen"
|
||||
forceClose: root.displayMode === "alwaysHide"
|
||||
onClicked: {
|
||||
|
||||
// You could open keyboard settings here if needed
|
||||
// For now, just show the current layout
|
||||
// You could open keyboard settings here if needed.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,13 +33,25 @@ Item {
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool compact: (Settings.data.bar.density === "compact")
|
||||
|
||||
readonly property bool autoHide: (widgetSettings.autoHide !== undefined) ? widgetSettings.autoHide : widgetMetadata.autoHide
|
||||
readonly property bool showAlbumArt: (widgetSettings.showAlbumArt !== undefined) ? widgetSettings.showAlbumArt : widgetMetadata.showAlbumArt
|
||||
readonly property bool showVisualizer: (widgetSettings.showVisualizer !== undefined) ? widgetSettings.showVisualizer : widgetMetadata.showVisualizer
|
||||
readonly property string visualizerType: (widgetSettings.visualizerType !== undefined && widgetSettings.visualizerType !== "") ? widgetSettings.visualizerType : widgetMetadata.visualizerType
|
||||
readonly property string scrollingMode: (widgetSettings.scrollingMode !== undefined) ? widgetSettings.scrollingMode : widgetMetadata.scrollingMode
|
||||
|
||||
// 6% of total width
|
||||
readonly property real minWidth: Math.max(1, screen.width * 0.06)
|
||||
readonly property real maxWidth: minWidth * 2
|
||||
// Fixed width - no expansion
|
||||
readonly property real widgetWidth: Math.max(1, screen.width * 0.06)
|
||||
|
||||
implicitHeight: visible ? ((barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)) : 0
|
||||
implicitWidth: visible ? ((barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (widgetWidth * scaling)) : 0
|
||||
|
||||
opacity: !autoHide || getTitle() !== "" ? 1.0 : 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
function getTitle() {
|
||||
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
|
||||
@@ -49,23 +61,6 @@ Item {
|
||||
return Math.round(Style.baseWidgetSize * 0.8 * scaling)
|
||||
}
|
||||
|
||||
function calculatedHorizontalWidth() {
|
||||
let total = Style.marginM * 2 * scaling // internal padding
|
||||
if (showAlbumArt) {
|
||||
total += 18 * scaling + 2 * scaling // album art + spacing
|
||||
} else {
|
||||
total += Style.fontSizeL * scaling + 2 * scaling // icon + spacing
|
||||
}
|
||||
total += Math.min(fullTitleMetrics.contentWidth, maxWidth * scaling) // title text
|
||||
// Row layout handles spacing between widgets
|
||||
return total
|
||||
}
|
||||
|
||||
implicitHeight: visible ? ((barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)) : 0
|
||||
implicitWidth: visible ? ((barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (rowLayout.implicitWidth + Style.marginM * 2 * scaling)) : 0
|
||||
|
||||
visible: MediaService.currentPlayer !== null && MediaService.canPlay
|
||||
|
||||
// A hidden text element to safely measure the full title width
|
||||
NText {
|
||||
id: fullTitleMetrics
|
||||
@@ -79,18 +74,11 @@ Item {
|
||||
visible: root.visible
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (rowLayout.implicitWidth + Style.marginM * 2 * scaling)
|
||||
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (widgetWidth * scaling)
|
||||
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : Math.round(Style.capsuleHeight * scaling)
|
||||
radius: (barPosition === "left" || barPosition === "right") ? width / 2 : Math.round(Style.radiusM * scaling)
|
||||
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
|
||||
|
||||
// Used to anchor the tooltip, so the tooltip does not move when the content expands
|
||||
Item {
|
||||
id: anchor
|
||||
height: parent.height
|
||||
width: 200 * scaling
|
||||
}
|
||||
|
||||
Item {
|
||||
id: mainContainer
|
||||
anchors.fill: parent
|
||||
@@ -100,14 +88,14 @@ Item {
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: showVisualizer && visualizerType == "linear" && MediaService.isPlaying
|
||||
active: showVisualizer && visualizerType == "linear"
|
||||
z: 0
|
||||
|
||||
sourceComponent: LinearSpectrum {
|
||||
width: mainContainer.width - Style.marginS * scaling
|
||||
height: 20 * scaling
|
||||
values: CavaService.values
|
||||
fillColor: Color.mOnSurfaceVariant
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
@@ -115,14 +103,14 @@ Item {
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: showVisualizer && visualizerType == "mirrored" && MediaService.isPlaying
|
||||
active: showVisualizer && visualizerType == "mirrored"
|
||||
z: 0
|
||||
|
||||
sourceComponent: MirroredSpectrum {
|
||||
width: mainContainer.width - Style.marginS * scaling
|
||||
height: mainContainer.height - Style.marginS * scaling
|
||||
values: CavaService.values
|
||||
fillColor: Color.mOnSurfaceVariant
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
@@ -130,14 +118,14 @@ Item {
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: showVisualizer && visualizerType == "wave" && MediaService.isPlaying
|
||||
active: showVisualizer && visualizerType == "wave"
|
||||
z: 0
|
||||
|
||||
sourceComponent: WaveSpectrum {
|
||||
width: mainContainer.width - Style.marginS * scaling
|
||||
height: mainContainer.height - Style.marginS * scaling
|
||||
values: CavaService.values
|
||||
fillColor: Color.mOnSurfaceVariant
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
@@ -145,9 +133,10 @@ Item {
|
||||
// Horizontal layout for top/bottom bars
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
visible: barPosition === "top" || barPosition === "bottom"
|
||||
visible: (barPosition === "top" || barPosition === "bottom") && getTitle() !== ""
|
||||
z: 1 // Above the visualizer
|
||||
|
||||
NIcon {
|
||||
@@ -180,24 +169,134 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
id: titleText
|
||||
|
||||
Item {
|
||||
id: titleContainer
|
||||
Layout.preferredWidth: {
|
||||
if (mouseArea.containsMouse) {
|
||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
|
||||
} else {
|
||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
|
||||
// Calculate available width based on other elements in the row
|
||||
var iconWidth = (windowIcon.visible ? (Style.fontSizeL * scaling + Style.marginS * scaling) : 0)
|
||||
var albumArtWidth = (showAlbumArt ? (18 * scaling + Style.marginS * scaling) : 0)
|
||||
var totalMargins = Style.marginXXS * scaling * 2
|
||||
var availableWidth = mainContainer.width - iconWidth - albumArtWidth - totalMargins
|
||||
return Math.max(20 * scaling, availableWidth)
|
||||
}
|
||||
Layout.maximumWidth: Layout.preferredWidth
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredHeight: titleText.height
|
||||
|
||||
clip: true
|
||||
|
||||
property bool isScrolling: false
|
||||
property bool isResetting: false
|
||||
property real textWidth: fullTitleMetrics.contentWidth
|
||||
property real containerWidth: width
|
||||
property bool needsScrolling: textWidth > containerWidth
|
||||
|
||||
// Timer for "always" mode with delay
|
||||
Timer {
|
||||
id: scrollStartTimer
|
||||
interval: 1000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (scrollingMode === "always" && titleContainer.needsScrolling) {
|
||||
titleContainer.isScrolling = true
|
||||
titleContainer.isResetting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
text: getTitle()
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mSecondary
|
||||
// Update scrolling state based on mode
|
||||
property var updateScrollingState: function () {
|
||||
if (scrollingMode === "never") {
|
||||
isScrolling = false
|
||||
isResetting = false
|
||||
} else if (scrollingMode === "always") {
|
||||
if (needsScrolling) {
|
||||
if (mouseArea.containsMouse) {
|
||||
isScrolling = false
|
||||
isResetting = true
|
||||
} else {
|
||||
scrollStartTimer.restart()
|
||||
}
|
||||
} else {
|
||||
scrollStartTimer.stop()
|
||||
isScrolling = false
|
||||
isResetting = false
|
||||
}
|
||||
} else if (scrollingMode === "hover") {
|
||||
if (mouseArea.containsMouse && needsScrolling) {
|
||||
isScrolling = true
|
||||
isResetting = false
|
||||
} else {
|
||||
isScrolling = false
|
||||
if (needsScrolling) {
|
||||
isResetting = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onWidthChanged: updateScrollingState()
|
||||
Component.onCompleted: updateScrollingState()
|
||||
|
||||
Connections {
|
||||
target: mouseArea
|
||||
function onContainsMouseChanged() {
|
||||
titleContainer.updateScrollingState()
|
||||
}
|
||||
}
|
||||
|
||||
// Scrolling content
|
||||
Item {
|
||||
id: scrollContainer
|
||||
height: parent.height
|
||||
width: childrenRect.width
|
||||
|
||||
property real scrollX: 0
|
||||
x: scrollX
|
||||
|
||||
Row {
|
||||
spacing: 50 * scaling // Gap between text copies
|
||||
|
||||
NText {
|
||||
id: titleText
|
||||
text: getTitle()
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
text: getTitle()
|
||||
font: titleText.font
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mOnSurface
|
||||
visible: titleContainer.needsScrolling && titleContainer.isScrolling
|
||||
}
|
||||
}
|
||||
|
||||
// Reset animation
|
||||
NumberAnimation on scrollX {
|
||||
running: titleContainer.isResetting
|
||||
to: 0
|
||||
duration: 300
|
||||
easing.type: Easing.OutQuad
|
||||
onFinished: {
|
||||
titleContainer.isResetting = false
|
||||
}
|
||||
}
|
||||
|
||||
// Seamless infinite scroll
|
||||
NumberAnimation on scrollX {
|
||||
id: infiniteScroll
|
||||
running: titleContainer.isScrolling && !titleContainer.isResetting
|
||||
from: 0
|
||||
to: -(titleContainer.textWidth + 50 * scaling) // Scroll one complete text width + gap
|
||||
duration: Math.max(4000, getTitle().length * 120)
|
||||
loops: Animation.Infinite
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
NumberAnimation {
|
||||
@@ -243,6 +342,10 @@ Item {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: mouse => {
|
||||
if (!MediaService.currentPlayer || !MediaService.canPlay) {
|
||||
return
|
||||
}
|
||||
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
MediaService.playPause()
|
||||
} else if (mouse.button == Qt.RightButton) {
|
||||
@@ -257,18 +360,14 @@ Item {
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
if (barPosition === "left" || barPosition === "right") {
|
||||
if ((tooltip.text !== "") && (barPosition === "left" || barPosition === "right")) {
|
||||
tooltip.show()
|
||||
} else if (tooltip.text !== "") {
|
||||
} else if ((tooltip.text !== "") && (scrollingMode === "never")) {
|
||||
tooltip.show()
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (barPosition === "left" || barPosition === "right") {
|
||||
tooltip.hide()
|
||||
} else {
|
||||
tooltip.hide()
|
||||
}
|
||||
tooltip.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,23 +376,23 @@ Item {
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
text: {
|
||||
if (barPosition === "left" || barPosition === "right") {
|
||||
return getTitle()
|
||||
} else {
|
||||
var str = ""
|
||||
if (MediaService.canGoNext) {
|
||||
str += "Right click for next.\n"
|
||||
}
|
||||
if (MediaService.canGoPrevious) {
|
||||
str += "Middle click for previous."
|
||||
}
|
||||
return str
|
||||
var title = getTitle()
|
||||
var controls = ""
|
||||
if (MediaService.canGoNext) {
|
||||
controls += "Right click for next.\n"
|
||||
}
|
||||
if (MediaService.canGoPrevious) {
|
||||
controls += "Middle click for previous."
|
||||
}
|
||||
if (controls !== "") {
|
||||
return title + "\n\n" + controls
|
||||
}
|
||||
return title
|
||||
}
|
||||
target: (barPosition === "left" || barPosition === "right") ? verticalLayout : anchor
|
||||
target: (barPosition === "left" || barPosition === "right") ? verticalLayout : mediaMini
|
||||
positionLeft: barPosition === "right"
|
||||
positionRight: barPosition === "left"
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
delay: 500
|
||||
delay: Style.tooltipDelay
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Commons
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Modules.Settings
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Bar.Extras
|
||||
@@ -93,11 +93,13 @@ Item {
|
||||
icon: getIcon()
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: Math.floor(AudioService.inputVolume * 100)
|
||||
text: Math.round(AudioService.inputVolume * 100)
|
||||
suffix: "%"
|
||||
forceOpen: displayMode === "alwaysShow"
|
||||
forceClose: displayMode === "alwaysHide"
|
||||
tooltipText: "Microphone: " + Math.round(AudioService.inputVolume * 100) + "%\nLeft click to toggle mute.\nRight click for settings.\nScroll to modify volume."
|
||||
tooltipText: I18n.tr("tooltips.microphone-volume-at", {
|
||||
"volume": Math.round(AudioService.inputVolume * 100)
|
||||
})
|
||||
|
||||
onWheel: function (delta) {
|
||||
wheelAccumulator += delta
|
||||
|
||||
@@ -4,7 +4,7 @@ import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Modules.Settings
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
@@ -22,7 +22,7 @@ NIconButton {
|
||||
colorBorderHover: Color.transparent
|
||||
|
||||
icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off"
|
||||
tooltipText: `Night light: ${Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "forced." : "enabled.") : "disabled."}\nLeft click to cycle (disabled → normal → forced).\nRight click to access settings.`
|
||||
tooltipText: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? I18n.tr("tooltips.night-light-forced") : I18n.tr("tooltips.night-light-enabled")) : I18n.tr("tooltips.night-light-disabled")
|
||||
onClicked: {
|
||||
if (!Settings.data.nightLight.enabled) {
|
||||
Settings.data.nightLight.enabled = true
|
||||
|
||||
@@ -39,7 +39,7 @@ NIconButton {
|
||||
function computeUnreadCount() {
|
||||
var since = lastSeenTs()
|
||||
var count = 0
|
||||
var model = NotificationService.historyModel
|
||||
var model = NotificationService.historyList
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
var item = model.get(i)
|
||||
var ts = item.timestamp instanceof Date ? item.timestamp.getTime() : item.timestamp
|
||||
@@ -52,7 +52,7 @@ NIconButton {
|
||||
baseSize: Style.capsuleHeight
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
|
||||
tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'."
|
||||
tooltipText: Settings.data.notifications.doNotDisturb ? I18n.tr("tooltips.open-notification-history-disable-dnd") : I18n.tr("tooltips.open-notification-history-enable-dnd")
|
||||
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
|
||||
@@ -16,7 +16,9 @@ NIconButton {
|
||||
visible: PowerProfileService.available
|
||||
|
||||
icon: PowerProfileService.getIcon()
|
||||
tooltipText: `Current power profile is "${PowerProfileService.getName()}".`
|
||||
tooltipText: I18n.tr("tooltips.power-profile", {
|
||||
"profile": PowerProfileService.getName()
|
||||
})
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
colorBg: (PowerProfileService.profile === PowerProfile.Balanced) ? (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) : Color.mPrimary
|
||||
colorFg: (PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnSurface : Color.mOnPrimary
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
// Screen Recording Indicator
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
icon: "camera-video"
|
||||
tooltipText: ScreenRecorderService.isRecording ? I18n.tr("tooltips.click-to-stop-recording") : I18n.tr("tooltips.click-to-start-recording")
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
baseSize: Style.capsuleHeight
|
||||
colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: ScreenRecorderService.toggleRecording()
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
// Screen Recording Indicator
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
visible: ScreenRecorderService.isRecording
|
||||
icon: "camera-video"
|
||||
tooltipText: "Screen recording is active.\nClick to stop recording."
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
baseSize: Style.capsuleHeight
|
||||
colorBg: Color.mPrimary
|
||||
colorFg: Color.mOnPrimary
|
||||
onClicked: ScreenRecorderService.toggleRecording()
|
||||
}
|
||||
@@ -14,10 +14,10 @@ NIconButton {
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
baseSize: Style.capsuleHeight
|
||||
icon: "power"
|
||||
tooltipText: "Power Settings"
|
||||
tooltipText: I18n.tr("tooltips.session-menu")
|
||||
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
colorFg: Color.mError
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: PanelService.getPanel("powerPanel")?.toggle()
|
||||
onClicked: PanelService.getPanel("sessionMenuPanel")?.toggle()
|
||||
}
|
||||
@@ -120,7 +120,9 @@ Rectangle {
|
||||
columnSpacing: Style.marginXXS * scaling
|
||||
|
||||
NText {
|
||||
text: isVertical ? `${SystemStatService.cpuTemp}°` : `${SystemStatService.cpuTemp}°C`
|
||||
text: I18n.tr("system.cpu-temperature", {
|
||||
"temp": SystemStatService.cpuTemp
|
||||
})
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
@@ -128,6 +130,7 @@ Rectangle {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
scale: isVertical ? Math.min(1.0, cpuTempContent.width / implicitWidth) : 1.0
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
}
|
||||
@@ -282,7 +285,9 @@ Rectangle {
|
||||
columnSpacing: isVertical ? (Style.marginXXS * scaling) : (Style.marginXS * scaling)
|
||||
|
||||
NText {
|
||||
text: `${SystemStatService.diskPercent}%`
|
||||
text: I18n.tr("system.disk-usage", {
|
||||
"percent": SystemStatService.diskPercent
|
||||
})
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: textSize
|
||||
font.weight: Style.fontWeightMedium
|
||||
|
||||
@@ -68,7 +68,7 @@ Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
source: AppIcons.iconForAppId(taskbarItem.modelData.appId)
|
||||
source: ThemeIcons.iconForAppId(taskbarItem.modelData.appId)
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
}
|
||||
@@ -104,7 +104,7 @@ Rectangle {
|
||||
|
||||
NTooltip {
|
||||
id: taskbarTooltip
|
||||
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App."
|
||||
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown app."
|
||||
target: taskbarItem
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Commons
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Modules.Settings
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Bar.Extras
|
||||
@@ -79,11 +79,13 @@ Item {
|
||||
rightOpen: BarService.getPillDirection(root)
|
||||
icon: getIcon()
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: Math.floor(AudioService.volume * 100)
|
||||
text: Math.round(AudioService.volume * 100)
|
||||
suffix: "%"
|
||||
forceOpen: displayMode === "alwaysShow"
|
||||
forceClose: displayMode === "alwaysHide"
|
||||
tooltipText: "Volume: " + Math.round(AudioService.volume * 100) + "%\nLeft click to toggle mute.\nRight click for settings.\nScroll to modify volume."
|
||||
tooltipText: I18n.tr("tooltips.volume-at", {
|
||||
"volume": Math.round(AudioService.volume * 100)
|
||||
})
|
||||
|
||||
onWheel: function (delta) {
|
||||
wheelAccumulator += delta
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
baseSize: Style.capsuleHeight
|
||||
compact: (Settings.data.bar.density === "compact")
|
||||
icon: "wallpaper-selector"
|
||||
tooltipText: I18n.tr("tooltips.open-wallpaper-selector")
|
||||
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this)
|
||||
}
|
||||
@@ -40,7 +40,7 @@ NIconButton {
|
||||
return "signal_wifi_bad"
|
||||
}
|
||||
}
|
||||
tooltipText: "Manage Wi-Fi."
|
||||
tooltipText: I18n.tr("tooltips.manage-wifi")
|
||||
onClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
|
||||
onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
|
||||
}
|
||||
|
||||
@@ -56,6 +56,10 @@ Item {
|
||||
property int horizontalPadding: Math.round(Style.marginS * scaling)
|
||||
property int spacingBetweenPills: Math.round(Style.marginXS * scaling)
|
||||
|
||||
// Wheel scroll handling
|
||||
property int wheelAccumulatedDelta: 0
|
||||
property bool wheelCooldown: false
|
||||
|
||||
signal workspaceChanged(int workspaceId, color accentColor)
|
||||
|
||||
implicitWidth: isVertical ? Math.round(Style.barHeight * scaling) : computeWidth()
|
||||
@@ -63,18 +67,14 @@ Item {
|
||||
|
||||
function getWorkspaceWidth(ws) {
|
||||
const d = Style.capsuleHeight * root.baseDimensionRatio
|
||||
if (ws.isFocused)
|
||||
return d * 2.5
|
||||
else
|
||||
return d
|
||||
const factor = ws.isFocused ? 2.2 : 1
|
||||
return d * factor * scaling
|
||||
}
|
||||
|
||||
function getWorkspaceHeight(ws) {
|
||||
const d = Style.capsuleHeight * root.baseDimensionRatio
|
||||
if (ws.isFocused)
|
||||
return d * 3
|
||||
else
|
||||
return d
|
||||
const factor = ws.isFocused ? 2.2 : 1
|
||||
return d * factor * scaling
|
||||
}
|
||||
|
||||
function computeWidth() {
|
||||
@@ -99,6 +99,28 @@ Item {
|
||||
return Math.round(total)
|
||||
}
|
||||
|
||||
function getFocusedLocalIndex() {
|
||||
for (var i = 0; i < localWorkspaces.count; i++) {
|
||||
if (localWorkspaces.get(i).isFocused === true)
|
||||
return i
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function switchByOffset(offset) {
|
||||
if (localWorkspaces.count === 0)
|
||||
return
|
||||
var current = getFocusedLocalIndex()
|
||||
if (current < 0)
|
||||
current = 0
|
||||
var next = (current + offset) % localWorkspaces.count
|
||||
if (next < 0)
|
||||
next = localWorkspaces.count - 1
|
||||
const ws = localWorkspaces.get(next)
|
||||
if (ws && ws.idx !== undefined)
|
||||
CompositorService.switchToWorkspace(ws.idx)
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
refreshWorkspaces()
|
||||
}
|
||||
@@ -111,7 +133,7 @@ Item {
|
||||
onHideUnoccupiedChanged: refreshWorkspaces()
|
||||
|
||||
Connections {
|
||||
target: WorkspaceService
|
||||
target: CompositorService
|
||||
function onWorkspacesChanged() {
|
||||
refreshWorkspaces()
|
||||
}
|
||||
@@ -120,8 +142,8 @@ Item {
|
||||
function refreshWorkspaces() {
|
||||
localWorkspaces.clear()
|
||||
if (screen !== null) {
|
||||
for (var i = 0; i < WorkspaceService.workspaces.count; i++) {
|
||||
const ws = WorkspaceService.workspaces.get(i)
|
||||
for (var i = 0; i < CompositorService.workspaces.count; i++) {
|
||||
const ws = CompositorService.workspaces.get(i)
|
||||
if (ws.output.toLowerCase() === screen.name.toLowerCase()) {
|
||||
if (hideUnoccupied && !ws.isOccupied && !ws.isFocused) {
|
||||
continue
|
||||
@@ -189,6 +211,46 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// Debounce timer for wheel interactions
|
||||
Timer {
|
||||
id: wheelDebounce
|
||||
interval: 150
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
root.wheelCooldown = false
|
||||
root.wheelAccumulatedDelta = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to switch workspaces
|
||||
WheelHandler {
|
||||
id: wheelHandler
|
||||
target: root
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
onWheel: function (event) {
|
||||
if (root.wheelCooldown)
|
||||
return
|
||||
// Prefer vertical delta, fall back to horizontal if needed
|
||||
var dy = event.angleDelta.y
|
||||
var dx = event.angleDelta.x
|
||||
var useDy = Math.abs(dy) >= Math.abs(dx)
|
||||
var delta = useDy ? dy : dx
|
||||
// One notch is typically 120
|
||||
root.wheelAccumulatedDelta += delta
|
||||
var step = 120
|
||||
if (Math.abs(root.wheelAccumulatedDelta) >= step) {
|
||||
var direction = root.wheelAccumulatedDelta > 0 ? -1 : 1
|
||||
// For vertical layout, natural mapping: wheel up -> previous, down -> next (already handled by sign)
|
||||
// For horizontal layout, same mapping using vertical wheel
|
||||
root.switchByOffset(direction)
|
||||
root.wheelCooldown = true
|
||||
wheelDebounce.restart()
|
||||
root.wheelAccumulatedDelta = 0
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal layout for top/bottom bars
|
||||
Row {
|
||||
id: pillRow
|
||||
@@ -203,7 +265,7 @@ Item {
|
||||
Item {
|
||||
id: workspacePillContainer
|
||||
width: root.getWorkspaceWidth(model)
|
||||
height: Style.capsuleHeight * root.baseDimensionRatio
|
||||
height: Style.capsuleHeight * root.baseDimensionRatio * scaling
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
@@ -235,7 +297,7 @@ Item {
|
||||
if (model.isActive || model.isOccupied)
|
||||
return Color.mOnSecondary
|
||||
|
||||
return Color.mOnSurface
|
||||
return Color.mOnSecondary
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,7 +312,7 @@ Item {
|
||||
if (model.isActive || model.isOccupied)
|
||||
return Color.mSecondary
|
||||
|
||||
return Color.mOutline
|
||||
return Qt.alpha(Color.mSecondary, 0.3)
|
||||
}
|
||||
scale: model.isFocused ? 1.0 : 0.9
|
||||
z: 0
|
||||
@@ -260,7 +322,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
WorkspaceService.switchToWorkspace(model.idx)
|
||||
CompositorService.switchToWorkspace(model.idx)
|
||||
}
|
||||
hoverEnabled: true
|
||||
}
|
||||
@@ -346,7 +408,7 @@ Item {
|
||||
model: localWorkspaces
|
||||
Item {
|
||||
id: workspacePillContainerVertical
|
||||
width: Style.capsuleHeight * root.baseDimensionRatio
|
||||
width: Style.capsuleHeight * root.baseDimensionRatio * scaling
|
||||
height: root.getWorkspaceHeight(model)
|
||||
|
||||
Rectangle {
|
||||
@@ -404,7 +466,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
WorkspaceService.switchToWorkspace(model.idx)
|
||||
CompositorService.switchToWorkspace(model.idx)
|
||||
}
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
@@ -32,12 +32,12 @@ NBox {
|
||||
color: Color.mPrimary
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
NText {
|
||||
text: "No media player detected"
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
// NText {
|
||||
// text: I18n.tr("system.no-media-player-detected")
|
||||
// color: Color.mOnSurfaceVariant
|
||||
// Layout.alignment: Qt.AlignHCenter
|
||||
// }
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
@@ -51,91 +51,71 @@ NBox {
|
||||
visible: MediaService.currentPlayer && MediaService.canPlay
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Player selector
|
||||
ComboBox {
|
||||
id: playerSelector
|
||||
// Player selector using NContextMenu
|
||||
Rectangle {
|
||||
id: playerSelectorButton
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.barHeight * 0.83 * scaling
|
||||
Layout.preferredHeight: Style.barHeight * scaling
|
||||
visible: MediaService.getAvailablePlayers().length > 1
|
||||
model: MediaService.getAvailablePlayers()
|
||||
textRole: "identity"
|
||||
currentIndex: MediaService.selectedPlayerIndex
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.transparent
|
||||
|
||||
background: Rectangle {
|
||||
visible: false
|
||||
// implicitWidth: 120 * scaling
|
||||
// implicitHeight: 30 * scaling
|
||||
color: Color.transparent
|
||||
border.color: playerSelector.activeFocus ? Color.mSecondary : Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
radius: Style.radiusM * scaling
|
||||
}
|
||||
property var currentPlayer: MediaService.getAvailablePlayers()[MediaService.selectedPlayerIndex]
|
||||
|
||||
contentItem: NText {
|
||||
visible: false
|
||||
leftPadding: Style.marginM * scaling
|
||||
rightPadding: playerSelector.indicator.width + playerSelector.spacing
|
||||
text: playerSelector.displayText
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurface
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
indicator: NIcon {
|
||||
x: playerSelector.width - width
|
||||
y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2
|
||||
icon: "caret-down"
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
color: Color.mOnSurface
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
|
||||
popup: Popup {
|
||||
id: popup
|
||||
x: playerSelector.width * 0.5
|
||||
y: playerSelector.height * 0.75
|
||||
width: playerSelector.width * 0.5
|
||||
implicitHeight: Math.min(160 * scaling, contentItem.implicitHeight + Style.marginM * scaling)
|
||||
padding: Style.marginS * scaling
|
||||
|
||||
contentItem: ListView {
|
||||
clip: true
|
||||
implicitHeight: contentHeight
|
||||
model: playerSelector.popup.visible ? playerSelector.delegateModel : null
|
||||
currentIndex: playerSelector.highlightedIndex
|
||||
ScrollIndicator.vertical: ScrollIndicator {}
|
||||
NIcon {
|
||||
icon: "caret-down"
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
radius: Style.radiusXS * scaling
|
||||
NText {
|
||||
text: playerSelectorButton.currentPlayer ? playerSelectorButton.currentPlayer.identity : ""
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
delegate: ItemDelegate {
|
||||
width: playerSelector.width
|
||||
contentItem: NText {
|
||||
text: modelData.identity
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: highlighted ? Color.mSurface : Color.mOnSurface
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
highlighted: playerSelector.highlightedIndex === index
|
||||
MouseArea {
|
||||
id: playerSelectorMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
background: Rectangle {
|
||||
width: popup.width - Style.marginS * scaling * 2
|
||||
color: highlighted ? Color.mSecondary : Color.transparent
|
||||
radius: Style.radiusXS * scaling
|
||||
onClicked: {
|
||||
// Create menu items from available players
|
||||
var menuItems = []
|
||||
var players = MediaService.getAvailablePlayers()
|
||||
for (var i = 0; i < players.length; i++) {
|
||||
menuItems.push({
|
||||
"label": players[i].identity,
|
||||
"action": i.toString(),
|
||||
"icon": "disc",
|
||||
"enabled": true,
|
||||
"visible": true
|
||||
})
|
||||
}
|
||||
playerContextMenu.model = menuItems
|
||||
playerContextMenu.openAtItem(playerSelectorButton, playerSelectorButton.width - playerContextMenu.width, playerSelectorButton.height)
|
||||
}
|
||||
}
|
||||
|
||||
onActivated: {
|
||||
MediaService.selectedPlayerIndex = currentIndex
|
||||
MediaService.updateCurrentPlayer()
|
||||
NContextMenu {
|
||||
id: playerContextMenu
|
||||
parent: root
|
||||
width: 200 * scaling
|
||||
|
||||
onTriggered: function (action) {
|
||||
var index = parseInt(action)
|
||||
if (!isNaN(index)) {
|
||||
MediaService.selectedPlayerIndex = index
|
||||
MediaService.updateCurrentPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +173,7 @@ NBox {
|
||||
NText {
|
||||
visible: MediaService.trackArtist !== ""
|
||||
text: MediaService.trackArtist
|
||||
color: Color.mOnSurface
|
||||
color: Color.mPrimary
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
@@ -300,7 +280,7 @@ NBox {
|
||||
// Previous button
|
||||
NIconButton {
|
||||
icon: "media-prev"
|
||||
tooltipText: "Previous Media"
|
||||
tooltipText: I18n.tr("tooltips.previous-media")
|
||||
visible: MediaService.canGoPrevious
|
||||
onClicked: MediaService.canGoPrevious ? MediaService.previous() : {}
|
||||
}
|
||||
@@ -308,7 +288,7 @@ NBox {
|
||||
// Play/Pause button
|
||||
NIconButton {
|
||||
icon: MediaService.isPlaying ? "media-pause" : "media-play"
|
||||
tooltipText: MediaService.isPlaying ? "Pause" : "Play"
|
||||
tooltipText: MediaService.isPlaying ? I18n.tr("tooltips.pause") : I18n.tr("tooltips.play")
|
||||
visible: (MediaService.canPlay || MediaService.canPause)
|
||||
onClicked: (MediaService.canPlay || MediaService.canPause) ? MediaService.playPause() : {}
|
||||
}
|
||||
@@ -316,7 +296,7 @@ NBox {
|
||||
// Next button
|
||||
NIconButton {
|
||||
icon: "media-next"
|
||||
tooltipText: "Next media"
|
||||
tooltipText: I18n.tr("tooltips.next-media")
|
||||
visible: MediaService.canGoNext
|
||||
onClicked: MediaService.canGoNext ? MediaService.next() : {}
|
||||
}
|
||||
@@ -324,7 +304,7 @@ NBox {
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Settings.data.audio.visualizerType == "linear" && MediaService.isPlaying
|
||||
active: Settings.data.audio.visualizerType == "linear"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
sourceComponent: LinearSpectrum {
|
||||
@@ -337,7 +317,7 @@ NBox {
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Settings.data.audio.visualizerType == "mirrored" && MediaService.isPlaying
|
||||
active: Settings.data.audio.visualizerType == "mirrored"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
sourceComponent: MirroredSpectrum {
|
||||
@@ -350,7 +330,7 @@ NBox {
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Settings.data.audio.visualizerType == "wave" && MediaService.isPlaying
|
||||
active: Settings.data.audio.visualizerType == "wave"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
sourceComponent: WaveSpectrum {
|
||||
+9
-3
@@ -26,7 +26,9 @@ NBox {
|
||||
// Performance
|
||||
NIconButton {
|
||||
icon: PowerProfileService.getIcon(PowerProfile.Performance)
|
||||
tooltipText: `Set "${PowerProfileService.getName(PowerProfile.Performance)}" power profile.`
|
||||
tooltipText: I18n.tr("tooltips.set-power-profile", {
|
||||
"profile": PowerProfileService.getName(PowerProfile.Performance)
|
||||
})
|
||||
enabled: hasPP
|
||||
opacity: enabled ? Style.opacityFull : Style.opacityMedium
|
||||
colorBg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant
|
||||
@@ -36,7 +38,9 @@ NBox {
|
||||
// Balanced
|
||||
NIconButton {
|
||||
icon: PowerProfileService.getIcon(PowerProfile.Balanced)
|
||||
tooltipText: `Set "${PowerProfileService.getName(PowerProfile.Balanced)}" power profile.`
|
||||
tooltipText: I18n.tr("tooltips.set-power-profile", {
|
||||
"profile": PowerProfileService.getName(PowerProfile.Balanced)
|
||||
})
|
||||
enabled: hasPP
|
||||
opacity: enabled ? Style.opacityFull : Style.opacityMedium
|
||||
colorBg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant
|
||||
@@ -46,7 +50,9 @@ NBox {
|
||||
// Eco
|
||||
NIconButton {
|
||||
icon: PowerProfileService.getIcon(PowerProfile.PowerSaver)
|
||||
tooltipText: `Set "${PowerProfileService.getName(PowerProfile.PowerSaver)}" power profile.`
|
||||
tooltipText: I18n.tr("tooltips.set-power-profile", {
|
||||
"profile": PowerProfileService.getName(PowerProfile.PowerSaver)
|
||||
})
|
||||
enabled: hasPP
|
||||
opacity: enabled ? Style.opacityFull : Style.opacityMedium
|
||||
colorBg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant
|
||||
+11
-9
@@ -4,8 +4,8 @@ import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Modules.SidePanel
|
||||
import qs.Modules.Settings
|
||||
import qs.Modules.ControlCenter
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -42,7 +42,9 @@ NBox {
|
||||
font.capitalization: Font.Capitalize
|
||||
}
|
||||
NText {
|
||||
text: `System uptime: ${uptimeText}`
|
||||
text: I18n.tr("system.uptime", {
|
||||
"uptime": uptimeText
|
||||
})
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
@@ -56,7 +58,7 @@ NBox {
|
||||
}
|
||||
NIconButton {
|
||||
icon: "settings"
|
||||
tooltipText: "Open settings."
|
||||
tooltipText: I18n.tr("tooltips.open-settings")
|
||||
onClicked: {
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.General
|
||||
settingsPanel.open()
|
||||
@@ -66,19 +68,19 @@ NBox {
|
||||
NIconButton {
|
||||
id: powerButton
|
||||
icon: "power"
|
||||
tooltipText: "Power menu."
|
||||
tooltipText: I18n.tr("tooltips.session-menu")
|
||||
onClicked: {
|
||||
powerPanel.open()
|
||||
sidePanel.close()
|
||||
sessionMenuPanel.open()
|
||||
controlCenterPanel.close()
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
id: closeButton
|
||||
icon: "close"
|
||||
tooltipText: "Close side panel."
|
||||
tooltipText: I18n.tr("tooltips.close")
|
||||
onClicked: {
|
||||
sidePanel.close()
|
||||
controlCenterPanel.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -3,7 +3,7 @@ import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Modules.Settings
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
@@ -24,7 +24,7 @@ NBox {
|
||||
NIconButton {
|
||||
icon: "camera-video"
|
||||
enabled: ScreenRecorderService.isAvailable
|
||||
tooltipText: ScreenRecorderService.isAvailable ? (ScreenRecorderService.isRecording ? "Stop screen recording." : "Start screen recording.") : "Screen recorder not installed."
|
||||
tooltipText: ScreenRecorderService.isAvailable ? (ScreenRecorderService.isRecording ? I18n.tr("tooltips.stop-screen-recording") : I18n.tr("tooltips.start-screen-recording")) : I18n.tr("tooltips.screen-recorder-not-installed")
|
||||
colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : Color.mSurfaceVariant
|
||||
colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary
|
||||
onClicked: {
|
||||
@@ -33,8 +33,8 @@ NBox {
|
||||
ScreenRecorderService.toggleRecording()
|
||||
// If we were not recording and we just initiated a start, close the panel
|
||||
if (!ScreenRecorderService.isRecording) {
|
||||
var panel = PanelService.getPanel("sidePanel")
|
||||
panel && panel.close()
|
||||
var panel = PanelService.getPanel("controlCenterPanel")
|
||||
panel?.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ NBox {
|
||||
// Idle Inhibitor
|
||||
NIconButton {
|
||||
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
|
||||
tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake." : "Enable keep awake."
|
||||
tooltipText: IdleInhibitorService.isInhibited ? I18n.tr("tooltips.disable-keep-awake") : I18n.tr("tooltips.enable-keep-awake")
|
||||
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant
|
||||
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary
|
||||
onClicked: {
|
||||
@@ -54,8 +54,8 @@ NBox {
|
||||
NIconButton {
|
||||
visible: Settings.data.wallpaper.enabled
|
||||
icon: "wallpaper-selector"
|
||||
tooltipText: "Left click: Open wallpaper selector.\nRight click: Set random wallpaper."
|
||||
onClicked: PanelService.getPanel("wallpaperSelector")?.toggle(this)
|
||||
tooltipText: I18n.tr("tooltips.wallpaper-selector")
|
||||
onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this)
|
||||
onRightClicked: WallpaperService.setRandomWallpaper()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Modules.SidePanel.Cards
|
||||
import qs.Modules.ControlCenter.Cards
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
+188
-16
@@ -13,6 +13,7 @@ Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: Item {
|
||||
id: root
|
||||
required property ShellScreen modelData
|
||||
property real scaling: ScalingService.getScreenScale(modelData)
|
||||
|
||||
@@ -25,6 +26,37 @@ Variants {
|
||||
}
|
||||
}
|
||||
|
||||
// Update dock apps when toplevels change
|
||||
Connections {
|
||||
target: ToplevelManager ? ToplevelManager.toplevels : null
|
||||
function onValuesChanged() {
|
||||
updateDockApps()
|
||||
}
|
||||
}
|
||||
|
||||
// Update dock apps when pinned apps change
|
||||
Connections {
|
||||
target: Settings.data.dock
|
||||
function onPinnedAppsChanged() {
|
||||
updateDockApps()
|
||||
}
|
||||
}
|
||||
|
||||
// Initial update when component is ready
|
||||
Component.onCompleted: {
|
||||
if (Settings.isLoaded && ToplevelManager) {
|
||||
updateDockApps()
|
||||
}
|
||||
}
|
||||
|
||||
// Update when Settings are loaded
|
||||
Connections {
|
||||
target: Settings
|
||||
function onSettingsLoaded() {
|
||||
updateDockApps()
|
||||
}
|
||||
}
|
||||
|
||||
// Shared properties between peek and dock windows
|
||||
readonly property bool autoHide: Settings.data.dock.autoHide
|
||||
readonly property int hideDelay: 500
|
||||
@@ -43,12 +75,66 @@ Variants {
|
||||
// Shared state between windows
|
||||
property bool dockHovered: false
|
||||
property bool anyAppHovered: false
|
||||
property bool menuHovered: false
|
||||
property bool hidden: autoHide
|
||||
property bool peekHovered: false
|
||||
|
||||
// Separate property to control Loader - stays true during animations
|
||||
property bool dockLoaded: !autoHide // Start loaded if autoHide is off
|
||||
|
||||
// Track the currently open context menu
|
||||
property var currentContextMenu: null
|
||||
|
||||
// Combined model of running apps and pinned apps
|
||||
property var dockApps: []
|
||||
|
||||
// Function to close any open context menu
|
||||
function closeAllContextMenus() {
|
||||
if (currentContextMenu && currentContextMenu.visible) {
|
||||
currentContextMenu.hide()
|
||||
}
|
||||
}
|
||||
|
||||
// Function to update the combined dock apps model
|
||||
function updateDockApps() {
|
||||
const runningApps = ToplevelManager ? (ToplevelManager.toplevels.values || []) : []
|
||||
const pinnedApps = Settings.data.dock.pinnedApps || []
|
||||
const combined = []
|
||||
const processedAppIds = new Set()
|
||||
|
||||
// Strategy: Maintain app positions as much as possible
|
||||
// 1. First pass: Add all running apps (both pinned and non-pinned) in their current order
|
||||
runningApps.forEach(toplevel => {
|
||||
if (toplevel && toplevel.appId) {
|
||||
const isPinned = pinnedApps.includes(toplevel.appId)
|
||||
const appType = isPinned ? "pinned-running" : "running"
|
||||
|
||||
combined.push({
|
||||
"type": appType,
|
||||
"toplevel": toplevel,
|
||||
"appId": toplevel.appId,
|
||||
"title": toplevel.title
|
||||
})
|
||||
processedAppIds.add(toplevel.appId)
|
||||
}
|
||||
})
|
||||
|
||||
// 2. Second pass: Add non-running pinned apps at the end
|
||||
pinnedApps.forEach(pinnedAppId => {
|
||||
if (!processedAppIds.has(pinnedAppId)) {
|
||||
// Pinned app that is not running
|
||||
combined.push({
|
||||
"type": "pinned",
|
||||
"toplevel": null,
|
||||
"appId": pinnedAppId,
|
||||
"title": pinnedAppId
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
dockApps = combined
|
||||
}
|
||||
|
||||
// Timer to unload dock after hide animation completes
|
||||
Timer {
|
||||
id: unloadTimer
|
||||
@@ -65,7 +151,7 @@ Variants {
|
||||
id: hideTimer
|
||||
interval: hideDelay
|
||||
onTriggered: {
|
||||
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) {
|
||||
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) {
|
||||
hidden = true
|
||||
unloadTimer.restart() // Start unload timer when hiding
|
||||
}
|
||||
@@ -137,7 +223,7 @@ Variants {
|
||||
|
||||
onExited: {
|
||||
peekHovered = false
|
||||
if (!hidden && !dockHovered && !anyAppHovered) {
|
||||
if (!hidden && !dockHovered && !anyAppHovered && !menuHovered) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
@@ -147,7 +233,7 @@ Variants {
|
||||
|
||||
// DOCK WINDOW
|
||||
Loader {
|
||||
active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && dockLoaded && ToplevelManager && (ToplevelManager.toplevels.values.length > 0)
|
||||
active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && dockLoaded && ToplevelManager && (dockApps.length > 0)
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
id: dockWindow
|
||||
@@ -235,10 +321,15 @@ Variants {
|
||||
|
||||
onExited: {
|
||||
dockHovered = false
|
||||
if (autoHide && !anyAppHovered && !peekHovered) {
|
||||
if (autoHide && !anyAppHovered && !peekHovered && !menuHovered) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
// Close any open context menu when clicking on the dock background
|
||||
closeAllContextMenus()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -247,10 +338,10 @@ Variants {
|
||||
height: parent.height - (Style.marginM * 2 * scaling)
|
||||
anchors.centerIn: parent
|
||||
|
||||
function getAppIcon(toplevel: Toplevel): string {
|
||||
if (!toplevel)
|
||||
function getAppIcon(appData): string {
|
||||
if (!appData || !appData.appId)
|
||||
return ""
|
||||
return AppIcons.iconForAppId(toplevel.appId?.toLowerCase())
|
||||
return ThemeIcons.iconForAppId(appData.appId?.toLowerCase())
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
@@ -260,7 +351,7 @@ Variants {
|
||||
anchors.centerIn: parent
|
||||
|
||||
Repeater {
|
||||
model: ToplevelManager ? ToplevelManager.toplevels : null
|
||||
model: dockApps
|
||||
|
||||
delegate: Item {
|
||||
id: appButton
|
||||
@@ -268,10 +359,19 @@ Variants {
|
||||
Layout.preferredHeight: iconSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData
|
||||
property bool isActive: modelData.toplevel && ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData.toplevel
|
||||
property bool hovered: appMouseArea.containsMouse
|
||||
property string appId: modelData ? modelData.appId : ""
|
||||
property string appTitle: modelData ? modelData.title : ""
|
||||
property string appTitle: modelData ? (modelData.title || modelData.appId) : ""
|
||||
property bool isRunning: modelData && (modelData.type === "running" || modelData.type === "pinned-running")
|
||||
|
||||
// Listen for the toplevel being closed
|
||||
Connections {
|
||||
target: modelData?.toplevel
|
||||
function onClosed() {
|
||||
Qt.callLater(root.updateDockApps)
|
||||
}
|
||||
}
|
||||
|
||||
// Individual tooltip for this app
|
||||
NTooltip {
|
||||
@@ -296,6 +396,9 @@ Variants {
|
||||
fillMode: Image.PreserveAspectFit
|
||||
cache: true
|
||||
|
||||
// Dim pinned apps that aren't running
|
||||
opacity: appButton.isRunning ? 1.0 : 0.6
|
||||
|
||||
scale: appButton.hovered ? 1.15 : 1.0
|
||||
|
||||
Behavior on scale {
|
||||
@@ -305,6 +408,13 @@ Variants {
|
||||
easing.overshoot: 1.2
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back if no icon
|
||||
@@ -314,6 +424,7 @@ Variants {
|
||||
icon: "question-mark"
|
||||
font.pointSize: iconSize * 0.7
|
||||
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
opacity: appButton.isRunning ? 1.0 : 0.6
|
||||
scale: appButton.hovered ? 1.15 : 1.0
|
||||
|
||||
Behavior on scale {
|
||||
@@ -323,6 +434,41 @@ Variants {
|
||||
easing.overshoot: 1.2
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Context menu popup
|
||||
DockMenu {
|
||||
id: contextMenu
|
||||
scaling: root.scaling
|
||||
onHoveredChanged: menuHovered = hovered
|
||||
onRequestClose: {
|
||||
contextMenu.hide()
|
||||
// Restart hide timer after menu action if auto-hide is enabled
|
||||
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
onAppClosed: root.updateDockApps // Force immediate dock update when app is closed
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
root.currentContextMenu = contextMenu
|
||||
} else if (root.currentContextMenu === contextMenu) {
|
||||
root.currentContextMenu = null
|
||||
// Reset menu hover state when menu becomes invisible
|
||||
menuHovered = false
|
||||
// Restart hide timer if conditions are met
|
||||
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -330,7 +476,7 @@ Variants {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
||||
|
||||
onEntered: {
|
||||
anyAppHovered = true
|
||||
@@ -347,17 +493,43 @@ Variants {
|
||||
onExited: {
|
||||
anyAppHovered = false
|
||||
appTooltip.hide()
|
||||
if (autoHide && !dockHovered && !peekHovered) {
|
||||
if (autoHide && !dockHovered && !peekHovered && !menuHovered) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: function (mouse) {
|
||||
if (mouse.button === Qt.MiddleButton && modelData?.close) {
|
||||
modelData.close()
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
// If right-clicking on the same app with an open context menu, close it
|
||||
if (root.currentContextMenu === contextMenu && contextMenu.visible) {
|
||||
root.closeAllContextMenus()
|
||||
return
|
||||
}
|
||||
// Close any other existing context menu first
|
||||
root.closeAllContextMenus()
|
||||
// Hide tooltip when showing context menu
|
||||
appTooltip.hide()
|
||||
contextMenu.show(appButton, modelData.toplevel || modelData)
|
||||
return
|
||||
}
|
||||
if (mouse.button === Qt.LeftButton && modelData?.activate) {
|
||||
modelData.activate()
|
||||
|
||||
// Close any existing context menu for non-right-click actions
|
||||
root.closeAllContextMenus()
|
||||
|
||||
// Check if toplevel is still valid (not a stale reference)
|
||||
const isValidToplevel = modelData?.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(modelData.toplevel)
|
||||
|
||||
if (mouse.button === Qt.MiddleButton && isValidToplevel && modelData.toplevel.close) {
|
||||
modelData.toplevel.close()
|
||||
Qt.callLater(root.updateDockApps) // Force immediate dock update
|
||||
} else if (mouse.button === Qt.LeftButton) {
|
||||
if (isValidToplevel && modelData.toplevel.activate) {
|
||||
// Running app - activate it
|
||||
modelData.toplevel.activate()
|
||||
} else if (modelData?.appId) {
|
||||
// Pinned app not running - launch it
|
||||
Quickshell.execDetached(["gtk-launch", modelData.appId])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PopupWindow {
|
||||
id: root
|
||||
|
||||
property var toplevel: null
|
||||
property Item anchorItem: null
|
||||
property real scaling: 1.0
|
||||
property bool hovered: menuMouseArea.containsMouse || activateMouseArea.containsMouse || pinMouseArea.containsMouse || closeMouseArea.containsMouse
|
||||
property var onAppClosed: null // Callback function for when an app is closed
|
||||
|
||||
signal requestClose
|
||||
|
||||
implicitWidth: 140 * scaling
|
||||
implicitHeight: contextMenuColumn.implicitHeight + (Style.marginM * scaling * 2)
|
||||
color: Color.transparent
|
||||
visible: false
|
||||
|
||||
// Helper functions for pin/unpin functionality
|
||||
function isAppPinned(appId) {
|
||||
if (!appId)
|
||||
return false
|
||||
const pinnedApps = Settings.data.dock.pinnedApps || []
|
||||
return pinnedApps.includes(appId)
|
||||
}
|
||||
|
||||
function toggleAppPin(appId) {
|
||||
if (!appId)
|
||||
return
|
||||
|
||||
let pinnedApps = (Settings.data.dock.pinnedApps || []).slice() // Create a copy
|
||||
const isPinned = pinnedApps.includes(appId)
|
||||
|
||||
if (isPinned) {
|
||||
// Unpin: remove from array
|
||||
pinnedApps = pinnedApps.filter(id => id !== appId)
|
||||
} else {
|
||||
// Pin: add to array
|
||||
pinnedApps.push(appId)
|
||||
}
|
||||
|
||||
// Update the settings
|
||||
Settings.data.dock.pinnedApps = pinnedApps
|
||||
}
|
||||
|
||||
anchor.item: anchorItem
|
||||
anchor.rect.x: anchorItem ? (anchorItem.width - implicitWidth) / 2 : 0
|
||||
anchor.rect.y: anchorItem ? -implicitHeight - (Style.marginM * scaling) : 0
|
||||
|
||||
function show(item, toplevelData) {
|
||||
if (!item) {
|
||||
Logger.warn("DockMenu", "anchorItem is undefined, won't show menu.")
|
||||
return
|
||||
}
|
||||
|
||||
anchorItem = item
|
||||
toplevel = toplevelData
|
||||
visible = true
|
||||
}
|
||||
|
||||
function hide() {
|
||||
visible = false
|
||||
}
|
||||
|
||||
// Close menu when clicking on background, track hover for the whole menu area
|
||||
MouseArea {
|
||||
id: menuMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: function (mouse) {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
root.hide() // Close on right-click
|
||||
} else {
|
||||
root.hide() // Close when clicking on the background (outside menu content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequences: ["Escape"]
|
||||
enabled: root.visible
|
||||
onActivated: root.hide()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusS * scaling
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
// Prevent clicks inside the menu from closing it
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
|
||||
} // Do nothing, just consume the click
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contextMenuColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
spacing: 0
|
||||
|
||||
// Focus item
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32 * scaling
|
||||
color: activateMouseArea.containsMouse ? Color.mTertiary : Color.transparent
|
||||
radius: Style.radiusXS * scaling
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "eye"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: activateMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("dock.menu.focus")
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: activateMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: activateMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
if (root.toplevel?.activate) {
|
||||
root.toplevel.activate()
|
||||
}
|
||||
root.requestClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pin/Unpin item
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32 * scaling
|
||||
color: pinMouseArea.containsMouse ? Color.mTertiary : Color.transparent
|
||||
radius: Style.radiusXS * scaling
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: {
|
||||
if (!root.toplevel)
|
||||
return "pin"
|
||||
return root.isAppPinned(root.toplevel.appId) ? "unpin" : "pin"
|
||||
}
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: pinMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: {
|
||||
if (!root.toplevel)
|
||||
return I18n.tr("dock.menu.pin")
|
||||
return root.isAppPinned(root.toplevel.appId) ? I18n.tr("dock.menu.unpin") : I18n.tr("dock.menu.pin")
|
||||
}
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: pinMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: pinMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
if (root.toplevel?.appId) {
|
||||
root.toggleAppPin(root.toplevel.appId)
|
||||
}
|
||||
//root.hide()
|
||||
root.requestClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close item
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32 * scaling
|
||||
color: closeMouseArea.containsMouse ? Color.mTertiary : Color.transparent
|
||||
radius: Style.radiusXS * scaling
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.marginS * scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NIcon {
|
||||
icon: "close"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: closeMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("dock.menu.close")
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: closeMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
// Check if toplevel is still valid before trying to close it
|
||||
const isValidToplevel = root.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(root.toplevel)
|
||||
|
||||
if (isValidToplevel && root.toplevel.close) {
|
||||
root.toplevel.close()
|
||||
// Trigger immediate dock update callback if provided
|
||||
if (root.onAppClosed && typeof root.onAppClosed === "function") {
|
||||
Qt.callLater(root.onAppClosed)
|
||||
}
|
||||
} else {
|
||||
Logger.warn("DockMenu", "Cannot close app - invalid toplevel reference")
|
||||
}
|
||||
root.hide()
|
||||
root.requestClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -245,7 +245,7 @@ NPanel {
|
||||
fontWeight: Style.fontWeightSemiBold
|
||||
|
||||
text: searchText
|
||||
placeholderText: "Search entries... or use > for commands"
|
||||
placeholderText: I18n.tr("placeholders.search-launcher")
|
||||
|
||||
onTextChanged: searchText = text
|
||||
|
||||
@@ -397,7 +397,7 @@ NPanel {
|
||||
sourceComponent: Component {
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: modelData.icon ? AppIcons.iconFromName(modelData.icon, "application-x-executable") : ""
|
||||
source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : ""
|
||||
visible: modelData.icon && source !== ""
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import "../../../Helpers/FuzzySort.js" as Fuzzysort
|
||||
|
||||
Item {
|
||||
property var launcher: null
|
||||
property string name: "Applications"
|
||||
property string name: I18n.tr("plugins.applications")
|
||||
property bool handleSearch: true
|
||||
property var entries: []
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import QtQuick
|
||||
import qs.Services
|
||||
import qs.Commons
|
||||
import "../../../Helpers/AdvancedMath.js" as AdvancedMath
|
||||
|
||||
Item {
|
||||
property var launcher: null
|
||||
property string name: "Calculator"
|
||||
property string name: I18n.tr("plugins.calculator")
|
||||
|
||||
function handleCommand(query) {
|
||||
// Handle >calc command or direct math expressions after >
|
||||
@@ -14,7 +15,7 @@ Item {
|
||||
function commands() {
|
||||
return [{
|
||||
"name": ">calc",
|
||||
"description": "Calculator - evaluate mathematical expressions",
|
||||
"description": I18n.tr("plugins.calculator-description"),
|
||||
"icon": "accessories-calculator",
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
@@ -36,8 +37,8 @@ Item {
|
||||
|
||||
if (!expression) {
|
||||
return [{
|
||||
"name": "Calculator",
|
||||
"description": "Enter a mathematical expression",
|
||||
"name": I18n.tr("plugins.calculator-name"),
|
||||
"description": I18n.tr("plugins.calculator-enter-expression"),
|
||||
"icon": "accessories-calculator",
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
@@ -59,7 +60,7 @@ Item {
|
||||
}]
|
||||
} catch (error) {
|
||||
return [{
|
||||
"name": "Error",
|
||||
"name": I18n.tr("plugins.calculator-error"),
|
||||
"description": error.message || "Invalid expression",
|
||||
"icon": "dialog-error",
|
||||
"isImage": false,
|
||||
|
||||
@@ -7,7 +7,7 @@ Item {
|
||||
id: root
|
||||
|
||||
// Plugin metadata
|
||||
property string name: "Clipboard History"
|
||||
property string name: I18n.tr("plugins.clipboard")
|
||||
property var launcher: null
|
||||
|
||||
// Plugin capabilities
|
||||
@@ -68,7 +68,7 @@ Item {
|
||||
function commands() {
|
||||
return [{
|
||||
"name": ">clip",
|
||||
"description": "Search clipboard history",
|
||||
"description": I18n.tr("plugins.clipboard-search-description"),
|
||||
"icon": "text-x-generic",
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
@@ -76,7 +76,7 @@ Item {
|
||||
}
|
||||
}, {
|
||||
"name": ">clip clear",
|
||||
"description": "Clear all clipboard history",
|
||||
"description": I18n.tr("plugins.clipboard-clear-description"),
|
||||
"icon": "text-x-generic",
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
@@ -99,8 +99,8 @@ Item {
|
||||
// Check if clipboard service is not active
|
||||
if (!ClipboardService.active) {
|
||||
return [{
|
||||
"name": "Clipboard History Disabled",
|
||||
"description": "Enable clipboard history in settings or install cliphist",
|
||||
"name": I18n.tr("plugins.clipboard-history-disabled"),
|
||||
"description": I18n.tr("plugins.clipboard-history-disabled-description"),
|
||||
"icon": "view-refresh",
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
@@ -110,8 +110,8 @@ Item {
|
||||
// Special command: clear
|
||||
if (query === "clear") {
|
||||
return [{
|
||||
"name": "Clear Clipboard History",
|
||||
"description": "Remove all items from clipboard history",
|
||||
"name": I18n.tr("plugins.clipboard-clear-history"),
|
||||
"description": I18n.tr("plugins.clipboard-clear-description-full"),
|
||||
"icon": "delete_sweep",
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
@@ -124,8 +124,8 @@ Item {
|
||||
// Show loading state if data is being loaded
|
||||
if (ClipboardService.loading || isWaitingForData) {
|
||||
return [{
|
||||
"name": "Loading clipboard history...",
|
||||
"description": "Please wait",
|
||||
"name": I18n.tr("plugins.clipboard-loading"),
|
||||
"description": I18n.tr("plugins.clipboard-loading-description"),
|
||||
"icon": "view-refresh",
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
@@ -140,8 +140,8 @@ Item {
|
||||
isWaitingForData = true
|
||||
ClipboardService.list(100)
|
||||
return [{
|
||||
"name": "Loading clipboard history...",
|
||||
"description": "Please wait",
|
||||
"name": I18n.tr("plugins.clipboard-loading"),
|
||||
"description": I18n.tr("plugins.clipboard-loading-description"),
|
||||
"icon": "view-refresh",
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
|
||||
@@ -199,7 +199,7 @@ Loader {
|
||||
z: 10
|
||||
|
||||
Loader {
|
||||
active: MediaService.isPlaying && Settings.data.audio.visualizerType == "linear"
|
||||
active: Settings.data.audio.visualizerType == "linear"
|
||||
anchors.centerIn: parent
|
||||
width: 160 * scaling
|
||||
height: 160 * scaling
|
||||
@@ -228,7 +228,7 @@ Loader {
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: MediaService.isPlaying && Settings.data.audio.visualizerType == "mirrored"
|
||||
active: Settings.data.audio.visualizerType == "mirrored"
|
||||
anchors.centerIn: parent
|
||||
width: 160 * scaling
|
||||
height: 160 * scaling
|
||||
@@ -258,7 +258,7 @@ Loader {
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: MediaService.isPlaying && Settings.data.audio.visualizerType == "wave"
|
||||
active: Settings.data.audio.visualizerType == "wave"
|
||||
anchors.centerIn: parent
|
||||
width: 160 * scaling
|
||||
height: 160 * scaling
|
||||
@@ -305,31 +305,6 @@ Loader {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width + 24 * scaling
|
||||
height: parent.height + 24 * scaling
|
||||
radius: width * 0.5
|
||||
color: Color.transparent
|
||||
border.color: Qt.alpha(Color.mPrimary, 0.3)
|
||||
border.width: Math.max(1, Style.borderM * scaling)
|
||||
z: -1
|
||||
visible: !MediaService.isPlaying
|
||||
SequentialAnimation on scale {
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 1.1
|
||||
duration: 1500
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: 1500
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NImageCircled {
|
||||
anchors.centerIn: parent
|
||||
width: 100 * scaling
|
||||
@@ -364,6 +339,7 @@ Loader {
|
||||
Rectangle {
|
||||
id: terminalBackground
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
radius: Style.radiusM * scaling
|
||||
color: Qt.alpha(Color.mSurface, 0.9)
|
||||
border.color: Color.mPrimary
|
||||
@@ -407,7 +383,7 @@ Loader {
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
NText {
|
||||
text: "SECURE TERMINAL"
|
||||
text: I18n.tr("lock-screen.secure-terminal")
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
@@ -479,7 +455,9 @@ Loader {
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
property int currentIndex: 0
|
||||
property string fullText: "Welcome back, " + Quickshell.env("USER") + "!"
|
||||
property string fullText: I18n.tr("system.welcome-back", {
|
||||
"user": Quickshell.env("USER")
|
||||
})
|
||||
|
||||
Timer {
|
||||
interval: Style.animationFast
|
||||
@@ -510,7 +488,7 @@ Loader {
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "sudo unlock-session"
|
||||
text: I18n.tr("lock-screen.unlock-command")
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
@@ -522,7 +500,7 @@ Loader {
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: "Password:"
|
||||
text: I18n.tr("lock-screen.password")
|
||||
color: Color.mPrimary
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
@@ -558,48 +536,60 @@ Loader {
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
id: asterisksText
|
||||
text: "*".repeat(passwordInput.text.length)
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
visible: passwordInput.activeFocus && !lockContext.unlockInProgress
|
||||
// Container for asterisks and cursor to control positioning
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: asterisksText.implicitHeight
|
||||
|
||||
SequentialAnimation {
|
||||
id: typingEffect
|
||||
NumberAnimation {
|
||||
target: passwordInput
|
||||
property: "scale"
|
||||
to: 1.01
|
||||
duration: 50
|
||||
}
|
||||
NumberAnimation {
|
||||
target: passwordInput
|
||||
property: "scale"
|
||||
to: 1.0
|
||||
duration: 50
|
||||
NText {
|
||||
id: asterisksText
|
||||
text: "*".repeat(passwordInput.text.length)
|
||||
color: Color.mOnSurface
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
visible: passwordInput.activeFocus && !lockContext.unlockInProgress
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
elide: Text.ElideRight
|
||||
|
||||
SequentialAnimation {
|
||||
id: typingEffect
|
||||
NumberAnimation {
|
||||
target: passwordInput
|
||||
property: "scale"
|
||||
to: 1.01
|
||||
duration: 50
|
||||
}
|
||||
NumberAnimation {
|
||||
target: passwordInput
|
||||
property: "scale"
|
||||
to: 1.0
|
||||
duration: 50
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 8 * scaling
|
||||
height: 20 * scaling
|
||||
color: Color.mPrimary
|
||||
visible: passwordInput.activeFocus
|
||||
Layout.leftMargin: -Style.marginS * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Rectangle {
|
||||
width: 8 * scaling
|
||||
height: 20 * scaling
|
||||
color: Color.mPrimary
|
||||
visible: passwordInput.activeFocus
|
||||
anchors.left: asterisksText.right
|
||||
anchors.leftMargin: Style.marginXS * scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: 500
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0.0
|
||||
duration: 500
|
||||
SequentialAnimation on opacity {
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: 500
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0.0
|
||||
duration: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -643,6 +633,7 @@ Loader {
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.bottomMargin: -10 * scaling
|
||||
Layout.fillWidth: true
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 120 * scaling
|
||||
Layout.preferredHeight: 40 * scaling
|
||||
@@ -731,6 +722,149 @@ Loader {
|
||||
}
|
||||
}
|
||||
|
||||
// ALARMING Easter Egg for long passwords
|
||||
Item {
|
||||
id: easterEggContainer
|
||||
anchors.fill: parent
|
||||
z: 1000
|
||||
|
||||
property bool easterEggTriggered: false
|
||||
|
||||
// Monitor password length
|
||||
Connections {
|
||||
target: passwordInput
|
||||
function onTextChanged() {
|
||||
if (passwordInput.text.length >= 25) {
|
||||
easterEggContainer.easterEggTriggered = true
|
||||
}
|
||||
}
|
||||
function onActiveFocusChanged() {
|
||||
if (!passwordInput.activeFocus) {
|
||||
easterEggContainer.easterEggTriggered = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also reset when authentication starts
|
||||
Connections {
|
||||
target: lockContext
|
||||
function onUnlockInProgressChanged() {
|
||||
if (lockContext.unlockInProgress) {
|
||||
easterEggContainer.easterEggTriggered = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scattered warning messages (game-style pop-ups)
|
||||
Repeater {
|
||||
model: easterEggContainer.easterEggTriggered && passwordInput.activeFocus && !lockContext.unlockInProgress ? 12 : 0
|
||||
|
||||
NText {
|
||||
property var messages: ["BREACH DETECTED", "SECURITY ALERT", "SYSTEM COMPROMISED", "ANOMALY DETECTED", "FIREWALL BREACH", "DEFENSE FAILING", "16 // 16 // 16", "THE ATLAS SEES ALL", "SIMULATION DETECTED", "WAKE UP", "16 16 16 16 16", "KZZT... 16... KZZT", "ERROR ERROR ERROR", "THEY'RE WATCHING", "16 MINUTES REMAIN"]
|
||||
|
||||
property real baseX: Math.random() * (parent.width - 300)
|
||||
property real baseY: Math.random() * (parent.height - 80)
|
||||
|
||||
text: messages[index % messages.length]
|
||||
color: Color.mError
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
|
||||
x: baseX
|
||||
y: baseY
|
||||
|
||||
// Better random positioning avoiding center terminal
|
||||
Component.onCompleted: {
|
||||
var centerX = parent.width / 2
|
||||
var centerY = parent.height / 2
|
||||
var avoidRadius = 350 * scaling
|
||||
|
||||
// If too close to center, push to random edge zones
|
||||
var distanceFromCenter = Math.sqrt((x - centerX) * (x - centerX) + (y - centerY) * (y - centerY))
|
||||
if (distanceFromCenter < avoidRadius) {
|
||||
// Pick a random edge zone
|
||||
var zone = Math.floor(Math.random() * 4)
|
||||
switch (zone) {
|
||||
case 0:
|
||||
// Top
|
||||
x = Math.random() * parent.width
|
||||
y = Math.random() * 100 * scaling
|
||||
break
|
||||
case 1:
|
||||
// Right
|
||||
x = parent.width - (50 + Math.random() * 200) * scaling
|
||||
y = Math.random() * parent.height
|
||||
break
|
||||
case 2:
|
||||
// Bottom
|
||||
x = Math.random() * parent.width
|
||||
y = parent.height - (50 + Math.random() * 100) * scaling
|
||||
break
|
||||
case 3:
|
||||
// Left
|
||||
x = Math.random() * 200 * scaling
|
||||
y = Math.random() * parent.height
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Add some random drift to make positioning more varied
|
||||
x += (Math.random() - 0.5) * 100 * scaling
|
||||
y += (Math.random() - 0.5) * 50 * scaling
|
||||
|
||||
// Ensure we stay within bounds
|
||||
x = Math.max(20 * scaling, Math.min(parent.width - 280 * scaling, x))
|
||||
y = Math.max(20 * scaling, Math.min(parent.height - 60 * scaling, y))
|
||||
}
|
||||
|
||||
// Simple pop-in animation
|
||||
SequentialAnimation on scale {
|
||||
loops: Animation.Infinite
|
||||
PauseAnimation {
|
||||
duration: index * 400 + Math.random() * 1000
|
||||
}
|
||||
NumberAnimation {
|
||||
from: 0
|
||||
to: 1.2
|
||||
duration: 300
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: 200
|
||||
}
|
||||
PauseAnimation {
|
||||
duration: 2000 + Math.random() * 3000
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0
|
||||
duration: 300
|
||||
}
|
||||
PauseAnimation {
|
||||
duration: 800 + Math.random() * 1200
|
||||
}
|
||||
}
|
||||
|
||||
// Gentle blinking effect
|
||||
SequentialAnimation on opacity {
|
||||
loops: Animation.Infinite
|
||||
PauseAnimation {
|
||||
duration: index * 200
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 0.6
|
||||
duration: 400 + Math.random() * 300
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 1.0
|
||||
duration: 300 + Math.random() * 200
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Power buttons at bottom right
|
||||
RowLayout {
|
||||
anchors.right: parent.right
|
||||
@@ -759,7 +893,7 @@ Loader {
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.top
|
||||
anchors.bottomMargin: 12 * scaling
|
||||
anchors.bottomMargin: Style.marginM * scaling
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
@@ -770,7 +904,7 @@ Loader {
|
||||
id: shutdownTooltipText
|
||||
anchors.margins: Style.marginM * scaling
|
||||
anchors.fill: parent
|
||||
text: "Shut down."
|
||||
text: I18n.tr("lock-screen.shut-down")
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
@@ -810,7 +944,7 @@ Loader {
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.top
|
||||
anchors.bottomMargin: 12 * scaling
|
||||
anchors.bottomMargin: Style.marginM * scaling
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
@@ -821,7 +955,7 @@ Loader {
|
||||
id: restartTooltipText
|
||||
anchors.margins: Style.marginM * scaling
|
||||
anchors.fill: parent
|
||||
text: "Restart."
|
||||
text: I18n.tr("lock-screen.restart")
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
@@ -862,7 +996,7 @@ Loader {
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.top
|
||||
anchors.bottomMargin: 12 * scaling
|
||||
anchors.bottomMargin: Style.marginM * scaling
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
@@ -873,7 +1007,7 @@ Loader {
|
||||
id: suspendTooltipText
|
||||
anchors.margins: Style.marginM * scaling
|
||||
anchors.fill: parent
|
||||
text: "Suspend."
|
||||
text: I18n.tr("lock-screen.suspend")
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
@@ -16,97 +16,113 @@ Variants {
|
||||
id: root
|
||||
|
||||
required property ShellScreen modelData
|
||||
readonly property real scaling: ScalingService.getScreenScale(modelData)
|
||||
property real scaling: ScalingService.getScreenScale(modelData)
|
||||
|
||||
// Access the notification model from the service
|
||||
property ListModel notificationModel: NotificationService.notificationModel
|
||||
|
||||
// Track notifications being removed for animation
|
||||
property var removingNotifications: ({})
|
||||
// Access the notification model from the service - UPDATED NAME
|
||||
property ListModel notificationModel: NotificationService.activeList
|
||||
|
||||
// If no notification display activated in settings, then show them all
|
||||
active: Settings.isLoaded && modelData && (NotificationService.notificationModel.count > 0) ? (Settings.data.notifications.monitors.includes(modelData.name) || (Settings.data.notifications.monitors.length === 0)) : false
|
||||
active: Settings.isLoaded && modelData && (Settings.data.notifications.monitors.includes(modelData.name) || (Settings.data.notifications.monitors.length === 0))
|
||||
|
||||
visible: (NotificationService.notificationModel.count > 0)
|
||||
Connections {
|
||||
target: ScalingService
|
||||
function onScaleChanged(screenName, scale) {
|
||||
if (root.modelData && screenName === root.modelData.name) {
|
||||
root.scaling = scale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
screen: modelData
|
||||
|
||||
WlrLayershell.namespace: "noctalia-notifications"
|
||||
WlrLayershell.layer: (Settings.isLoaded && Settings.data && Settings.data.notifications && Settings.data.notifications.alwaysOnTop) ? WlrLayer.Overlay : WlrLayer.Top
|
||||
|
||||
color: Color.transparent
|
||||
|
||||
// Position based on bar location - always at top
|
||||
anchors.top: true
|
||||
anchors.right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
|
||||
anchors.left: Settings.data.bar.position === "left"
|
||||
readonly property string location: (Settings.isLoaded && Settings.data && Settings.data.notifications && Settings.data.notifications.location) ? Settings.data.notifications.location : "top_right"
|
||||
readonly property bool isTop: (location === "top") || (location.length >= 3 && location.substring(0, 3) === "top")
|
||||
readonly property bool isBottom: (location === "bottom") || (location.length >= 6 && location.substring(0, 6) === "bottom")
|
||||
readonly property bool isLeft: location.indexOf("_left") >= 0
|
||||
readonly property bool isRight: location.indexOf("_right") >= 0
|
||||
readonly property bool isCentered: (location === "top" || location === "bottom")
|
||||
|
||||
// Anchor selection based on location (window edges)
|
||||
anchors.top: isTop
|
||||
anchors.bottom: isBottom
|
||||
anchors.left: isLeft
|
||||
anchors.right: isRight
|
||||
|
||||
// Margins depending on bar position and chosen location
|
||||
margins.top: {
|
||||
switch (Settings.data.bar.position) {
|
||||
case "top":
|
||||
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
|
||||
default:
|
||||
return Style.marginM * scaling
|
||||
if (!(anchors.top))
|
||||
return 0
|
||||
var base = Style.marginM * scaling
|
||||
if (Settings.data.bar.position === "top") {
|
||||
var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
|
||||
return (Style.barHeight * scaling) + base + floatExtraV
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
margins.bottom: {
|
||||
switch (Settings.data.bar.position) {
|
||||
case "bottom":
|
||||
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
|
||||
default:
|
||||
if (!(anchors.bottom))
|
||||
return 0
|
||||
var base = Style.marginM * scaling
|
||||
if (Settings.data.bar.position === "bottom") {
|
||||
var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
|
||||
return (Style.barHeight * scaling) + base + floatExtraV
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
margins.left: {
|
||||
switch (Settings.data.bar.position) {
|
||||
case "left":
|
||||
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0)
|
||||
default:
|
||||
if (!(anchors.left))
|
||||
return 0
|
||||
var base = Style.marginM * scaling
|
||||
if (Settings.data.bar.position === "left") {
|
||||
var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
|
||||
return (Style.barHeight * scaling) + base + floatExtraH
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
margins.right: {
|
||||
switch (Settings.data.bar.position) {
|
||||
case "right":
|
||||
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0)
|
||||
case "top":
|
||||
case "bottom":
|
||||
return Style.marginM * scaling
|
||||
default:
|
||||
if (!(anchors.right))
|
||||
return 0
|
||||
var base = Style.marginM * scaling
|
||||
if (Settings.data.bar.position === "right") {
|
||||
var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
|
||||
return (Style.barHeight * scaling) + base + floatExtraH
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
implicitWidth: 360 * scaling
|
||||
implicitHeight: Math.min(notificationStack.implicitHeight, (NotificationService.maxVisible * 120) * scaling)
|
||||
//WlrLayershell.layer: WlrLayer.Overlay
|
||||
implicitHeight: notificationStack.implicitHeight
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// Connect to animation signal from service
|
||||
// Connect to animation signal from service - UPDATED TO USE ID
|
||||
Component.onCompleted: {
|
||||
NotificationService.animateAndRemove.connect(function (notification, index) {
|
||||
// Prefer lookup by identity to avoid index mismatches
|
||||
NotificationService.animateAndRemove.connect(function (notificationId) {
|
||||
// Find the delegate by notification ID
|
||||
var delegate = null
|
||||
if (notificationStack && notificationStack.children && notificationStack.children.length > 0) {
|
||||
for (var i = 0; i < notificationStack.children.length; i++) {
|
||||
var child = notificationStack.children[i]
|
||||
if (child && child.model && child.model.rawNotification === notification) {
|
||||
if (child && child.notificationId === notificationId) {
|
||||
delegate = child
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to index if identity lookup failed
|
||||
if (!delegate && notificationStack && notificationStack.children && notificationStack.children[index]) {
|
||||
delegate = notificationStack.children[index]
|
||||
}
|
||||
|
||||
if (delegate && delegate.animateOut) {
|
||||
delegate.animateOut()
|
||||
} else {
|
||||
// As a last resort, force-remove without animation to avoid stuck popups
|
||||
NotificationService.forceRemoveNotification(notification)
|
||||
// Force removal without animation as fallback
|
||||
NotificationService.dismissActiveNotification(notificationId)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -114,10 +130,12 @@ Variants {
|
||||
// Main notification container
|
||||
ColumnLayout {
|
||||
id: notificationStack
|
||||
// Position based on bar location - always at top
|
||||
anchors.top: parent.top
|
||||
anchors.right: (Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom") ? parent.right : undefined
|
||||
anchors.left: Settings.data.bar.position === "left" ? parent.left : undefined
|
||||
// Anchor the stack inside the window based on chosen location
|
||||
anchors.top: parent.isTop ? parent.top : undefined
|
||||
anchors.bottom: parent.isBottom ? parent.bottom : undefined
|
||||
anchors.left: parent.isLeft ? parent.left : undefined
|
||||
anchors.right: parent.isRight ? parent.right : undefined
|
||||
anchors.horizontalCenter: parent.isCentered ? parent.horizontalCenter : undefined
|
||||
spacing: Style.marginS * scaling
|
||||
width: 360 * scaling
|
||||
visible: true
|
||||
@@ -126,6 +144,9 @@ Variants {
|
||||
Repeater {
|
||||
model: notificationModel
|
||||
delegate: Rectangle {
|
||||
// Store the notification ID for reference
|
||||
property string notificationId: model.id
|
||||
|
||||
Layout.preferredWidth: 360 * scaling
|
||||
Layout.preferredHeight: notificationLayout.implicitHeight + (Style.marginL * 2 * scaling)
|
||||
Layout.maximumHeight: Layout.preferredHeight
|
||||
@@ -135,6 +156,32 @@ Variants {
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
color: Color.mSurface
|
||||
|
||||
Rectangle {
|
||||
id: progressBar
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 2 * scaling
|
||||
color: "transparent"
|
||||
|
||||
property real availableWidth: parent.width - (2 * parent.radius)
|
||||
|
||||
Rectangle {
|
||||
x: parent.parent.radius + (parent.availableWidth * (1 - model.progress)) / 2
|
||||
width: parent.availableWidth * model.progress
|
||||
height: parent.height
|
||||
color: {
|
||||
if (model.urgency === NotificationUrgency.Critical || model.urgency === 2)
|
||||
return Color.mError
|
||||
else if (model.urgency === NotificationUrgency.Low || model.urgency === 0)
|
||||
return Color.mOnSurface
|
||||
else
|
||||
return Color.mPrimary
|
||||
}
|
||||
antialiasing: true
|
||||
}
|
||||
}
|
||||
|
||||
// Animation properties
|
||||
property real scaleValue: 0.8
|
||||
property real opacityValue: 0.0
|
||||
@@ -174,14 +221,14 @@ Variants {
|
||||
interval: Style.animationSlow
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
NotificationService.forceRemoveNotification(model.rawNotification)
|
||||
// Use the new API method with notification ID
|
||||
NotificationService.dismissActiveNotification(notificationId)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this notification is being removed
|
||||
onIsRemovingChanged: {
|
||||
if (isRemoving) {
|
||||
// Remove from model after animation completes
|
||||
removalTimer.start()
|
||||
}
|
||||
}
|
||||
@@ -191,7 +238,6 @@ Variants {
|
||||
NumberAnimation {
|
||||
duration: Style.animationSlow
|
||||
easing.type: Easing.OutExpo
|
||||
//easing.type: Easing.OutBack looks better but notification get clipped on all sides
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,44 +255,28 @@ Variants {
|
||||
anchors.rightMargin: (Style.marginM + 32) * scaling // Leave space for close button
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Header section with app name and timestamp
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NText {
|
||||
text: `${(model.appName || model.desktopEntry) || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}`
|
||||
color: Color.mSecondary
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 6 * scaling
|
||||
Layout.preferredHeight: 6 * scaling
|
||||
radius: Style.radiusXS * scaling
|
||||
color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// Main content section
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Image
|
||||
NImageCircled {
|
||||
Layout.preferredWidth: 40 * scaling
|
||||
Layout.preferredHeight: 40 * scaling
|
||||
Layout.alignment: Qt.AlignTop
|
||||
imagePath: model.image && model.image !== "" ? model.image : ""
|
||||
borderColor: Color.transparent
|
||||
borderWidth: 0
|
||||
visible: (model.image && model.image !== "")
|
||||
ColumnLayout {
|
||||
// For real-time notification always show the original image
|
||||
// as the cached version is most likely still processing.
|
||||
NImageCircled {
|
||||
Layout.preferredWidth: 40 * scaling
|
||||
Layout.preferredHeight: 40 * scaling
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.topMargin: 30 * scaling
|
||||
imagePath: model.originalImage || ""
|
||||
borderColor: Color.transparent
|
||||
borderWidth: 0
|
||||
fallbackIcon: "bell"
|
||||
fallbackIconSize: 24 * scaling
|
||||
}
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
// Text content
|
||||
@@ -254,8 +284,39 @@ Variants {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Header section with app name and timestamp
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 6 * scaling
|
||||
Layout.preferredHeight: 6 * scaling
|
||||
radius: Style.radiusXS * scaling
|
||||
color: {
|
||||
if (model.urgency === NotificationUrgency.Critical || model.urgency === 2)
|
||||
return Color.mError
|
||||
else if (model.urgency === NotificationUrgency.Low || model.urgency === 0)
|
||||
return Color.mOnSurface
|
||||
else
|
||||
return Color.mPrimary
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: `${model.appName || I18n.tr("system.unknown-app")} · ${Time.formatRelativeTime(model.timestamp)}`
|
||||
color: Color.mSecondary
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: model.summary || "No summary"
|
||||
text: model.summary || I18n.tr("general.no-summary")
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
@@ -264,6 +325,7 @@ Variants {
|
||||
Layout.fillWidth: true
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
NText {
|
||||
@@ -277,57 +339,65 @@ Variants {
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notification actions
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
visible: model.rawNotification && model.rawNotification.actions && model.rawNotification.actions.length > 0
|
||||
// Notification actions
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
Layout.topMargin: Style.marginM * scaling
|
||||
|
||||
property var notificationActions: model.rawNotification ? model.rawNotification.actions : []
|
||||
// Store the notification ID for access in button delegates
|
||||
property string parentNotificationId: notificationId
|
||||
|
||||
Repeater {
|
||||
model: parent.notificationActions
|
||||
|
||||
delegate: NButton {
|
||||
text: {
|
||||
var actionText = modelData.text || "Open"
|
||||
// If text contains comma, take the part after the comma (the display text)
|
||||
if (actionText.includes(",")) {
|
||||
return actionText.split(",")[1] || actionText
|
||||
// Parse actions from JSON string
|
||||
property var parsedActions: {
|
||||
try {
|
||||
return model.actionsJson ? JSON.parse(model.actionsJson) : []
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
return actionText
|
||||
}
|
||||
fontSize: Style.fontSizeS * scaling
|
||||
backgroundColor: Color.mPrimary
|
||||
textColor: Color.mOnPrimary
|
||||
hoverColor: Color.mSecondary
|
||||
pressColor: Color.mTertiary
|
||||
outlined: false
|
||||
customHeight: 32 * scaling
|
||||
Layout.preferredHeight: 32 * scaling
|
||||
visible: parsedActions.length > 0
|
||||
|
||||
onClicked: {
|
||||
if (modelData && modelData.invoke) {
|
||||
modelData.invoke()
|
||||
Repeater {
|
||||
model: parent.parsedActions
|
||||
|
||||
delegate: NButton {
|
||||
property var actionData: modelData
|
||||
|
||||
text: {
|
||||
var actionText = actionData.text || "Open"
|
||||
// If text contains comma, take the part after the comma (the display text)
|
||||
if (actionText.includes(",")) {
|
||||
return actionText.split(",")[1] || actionText
|
||||
}
|
||||
return actionText
|
||||
}
|
||||
fontSize: Style.fontSizeS * scaling
|
||||
backgroundColor: Color.mPrimary
|
||||
textColor: hovered ? Color.mOnTertiary : Color.mOnPrimary
|
||||
hoverColor: Color.mTertiary
|
||||
outlined: false
|
||||
Layout.preferredHeight: 24 * scaling
|
||||
onClicked: {
|
||||
NotificationService.invokeAction(parent.parentNotificationId, actionData.identifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer to push buttons to the left
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer to push buttons to the left if needed
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close button positioned absolutely
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close."
|
||||
tooltipText: I18n.tr("tooltips.close")
|
||||
baseSize: Style.baseWidgetSize * 0.6
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Style.marginM * scaling
|
||||
|
||||
@@ -13,7 +13,7 @@ NPanel {
|
||||
id: root
|
||||
|
||||
preferredWidth: 380
|
||||
preferredHeight: 500
|
||||
preferredHeight: 480
|
||||
panelKeyboardFocus: true
|
||||
|
||||
panelContent: Rectangle {
|
||||
@@ -37,7 +37,7 @@ NPanel {
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Notification History"
|
||||
text: I18n.tr("notifications.panel.title")
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
@@ -46,29 +46,27 @@ NPanel {
|
||||
|
||||
NIconButton {
|
||||
icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
|
||||
tooltipText: Settings.data.notifications.doNotDisturb ? "'Do Not Disturb' is enabled." : "'Do Not Disturb' is disabled."
|
||||
tooltipText: Settings.data.notifications.doNotDisturb ? I18n.tr("tooltips.do-not-disturb-enabled") : I18n.tr("tooltips.do-not-disturb-disabled")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
|
||||
onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "trash"
|
||||
tooltipText: "Clear history."
|
||||
tooltipText: I18n.tr("tooltips.clear-history")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
NotificationService.clearHistory()
|
||||
// Close panel as there is nothing more to see.
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close."
|
||||
tooltipText: I18n.tr("tooltips.close")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
root.close()
|
||||
}
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +79,7 @@ NPanel {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: NotificationService.historyModel.count === 0
|
||||
visible: NotificationService.historyList.count === 0
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
Item {
|
||||
@@ -96,14 +94,14 @@ NPanel {
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "No notifications"
|
||||
text: I18n.tr("notifications.panel.no-notifications")
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Your notifications will show up here as they arrive."
|
||||
text: I18n.tr("notifications.panel.description")
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
@@ -125,13 +123,15 @@ NPanel {
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
|
||||
model: NotificationService.historyModel
|
||||
model: NotificationService.historyList
|
||||
spacing: Style.marginM * scaling
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
visible: NotificationService.historyModel.count > 0
|
||||
visible: NotificationService.historyList.count > 0
|
||||
|
||||
delegate: Rectangle {
|
||||
property string notificationId: model.id
|
||||
|
||||
width: notificationList.width
|
||||
height: notificationLayout.implicitHeight + (Style.marginM * scaling * 2)
|
||||
radius: Style.radiusM * scaling
|
||||
@@ -139,36 +139,87 @@ NPanel {
|
||||
border.color: Qt.alpha(Color.mOutline, Style.opacityMedium)
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
// Smooth color transition on hover
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: notificationLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// App icon (same style as popup)
|
||||
NImageCircled {
|
||||
Layout.preferredWidth: 28 * scaling
|
||||
Layout.preferredHeight: 28 * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
// Prefer stable themed icons over transient image paths
|
||||
imagePath: (appIcon && appIcon !== "") ? (AppIcons.iconFromName(appIcon, "application-x-executable") || appIcon) : ((AppIcons.iconForAppId(desktopEntry || appName, "application-x-executable") || (image && image !== "" ? image : AppIcons.iconFromName("application-x-executable", "application-x-executable"))))
|
||||
borderColor: Color.transparent
|
||||
borderWidth: 0
|
||||
visible: true
|
||||
ColumnLayout {
|
||||
NImageCircled {
|
||||
Layout.preferredWidth: 40 * scaling
|
||||
Layout.preferredHeight: 40 * scaling
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.topMargin: 20 * scaling
|
||||
imagePath: model.cachedImage || model.originalImage || ""
|
||||
borderColor: Color.transparent
|
||||
borderWidth: 0
|
||||
fallbackIcon: "bell"
|
||||
fallbackIconSize: 24 * scaling
|
||||
}
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
// Notification content column
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.maximumWidth: notificationList.width - (Style.marginM * scaling * 4) // Account for margins and delete button
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: Style.marginXS * scaling
|
||||
|
||||
// Header row with app name and timestamp
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Urgency indicator
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 6 * scaling
|
||||
Layout.preferredHeight: 6 * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
radius: 3 * scaling
|
||||
visible: model.urgency !== 1
|
||||
color: {
|
||||
if (model.urgency === 2)
|
||||
return Color.mError
|
||||
else if (model.urgency === 0)
|
||||
return Color.mOnSurfaceVariant
|
||||
else
|
||||
return Color.transparent
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: model.appName || "Unknown App"
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mSecondary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: Time.formatRelativeTime(model.timestamp)
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mSecondary
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
NText {
|
||||
text: (summary || "No summary").substring(0, 100)
|
||||
text: model.summary || I18n.tr("general.no-summary")
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Font.Medium
|
||||
color: Color.mPrimary
|
||||
color: Color.mOnSurface
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
@@ -176,10 +227,11 @@ NPanel {
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Body
|
||||
NText {
|
||||
text: (body || "").substring(0, 150)
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurface
|
||||
text: model.body || ""
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
@@ -187,36 +239,21 @@ NPanel {
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
NText {
|
||||
text: NotificationService.formatTimestamp(timestamp)
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// Delete button
|
||||
NIconButton {
|
||||
icon: "trash"
|
||||
tooltipText: "Delete notification."
|
||||
tooltipText: I18n.tr("tooltips.delete-notification")
|
||||
baseSize: Style.baseWidgetSize * 0.7
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
onClicked: {
|
||||
Logger.log("NotificationHistory", "Removing notification:", summary)
|
||||
NotificationService.historyModel.remove(index)
|
||||
NotificationService.saveHistory()
|
||||
// Remove from history using the service API
|
||||
NotificationService.removeFromHistory(notificationId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: notificationMouseArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: Style.marginXL * scaling
|
||||
hoverEnabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
// Unified OSD component - handles both volume and brightness with a single instance
|
||||
// Loader activates only when showing OSD, deactivates when hidden to save resources
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: Loader {
|
||||
id: root
|
||||
|
||||
required property ShellScreen modelData
|
||||
property real scaling: ScalingService.getScreenScale(modelData)
|
||||
|
||||
// Access the notification model from the service
|
||||
property ListModel notificationModel: NotificationService.activeList
|
||||
|
||||
// If no notification display activated in settings, then show them all
|
||||
property bool canShowOnThisScreen: Settings.isLoaded && modelData && (Settings.data.notifications.monitors.includes(modelData.name) || (Settings.data.notifications.monitors.length === 0))
|
||||
|
||||
// Loader is only active when actually showing something
|
||||
active: false
|
||||
|
||||
// Current OSD display state
|
||||
property string currentOSDType: "" // "volume", "brightness", or ""
|
||||
|
||||
// Volume properties
|
||||
readonly property real currentVolume: AudioService.volume
|
||||
readonly property bool isMuted: AudioService.muted
|
||||
property bool volumeInitialized: false
|
||||
property bool muteInitialized: false
|
||||
|
||||
// Brightness properties
|
||||
property bool brightnessInitialized: false
|
||||
readonly property real currentBrightness: {
|
||||
if (BrightnessService.monitors.length > 0) {
|
||||
return BrightnessService.monitors[0].brightness || 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Get appropriate icon based on current OSD type
|
||||
function getIcon() {
|
||||
if (currentOSDType === "volume") {
|
||||
if (AudioService.muted) {
|
||||
return "volume-mute"
|
||||
}
|
||||
return (AudioService.volume <= Number.EPSILON) ? "volume-zero" : (AudioService.volume <= 0.5) ? "volume-low" : "volume-high"
|
||||
} else if (currentOSDType === "brightness") {
|
||||
return currentBrightness <= 0.5 ? "brightness-low" : "brightness-high"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Get current value (0-1 range)
|
||||
function getCurrentValue() {
|
||||
if (currentOSDType === "volume") {
|
||||
return isMuted ? 0 : currentVolume
|
||||
} else if (currentOSDType === "brightness") {
|
||||
return currentBrightness
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Get display percentage
|
||||
function getDisplayPercentage() {
|
||||
if (currentOSDType === "volume") {
|
||||
return isMuted ? "0%" : Math.round(currentVolume * 100) + "%"
|
||||
} else if (currentOSDType === "brightness") {
|
||||
return Math.round(currentBrightness * 100) + "%"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Get progress bar color
|
||||
function getProgressColor() {
|
||||
if (currentOSDType === "volume") {
|
||||
if (isMuted)
|
||||
return Color.mError
|
||||
if (currentVolume > 1.0)
|
||||
return Color.mError
|
||||
return Color.mPrimary
|
||||
}
|
||||
return Color.mPrimary
|
||||
}
|
||||
|
||||
// Get icon color
|
||||
function getIconColor() {
|
||||
if (currentOSDType === "volume" && isMuted) {
|
||||
return Color.mError
|
||||
}
|
||||
return Color.mOnSurface
|
||||
}
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
screen: modelData
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
}
|
||||
|
||||
implicitWidth: 320 * root.scaling
|
||||
implicitHeight: osdItem.height
|
||||
|
||||
// Set margins based on bar position
|
||||
margins.top: {
|
||||
switch (Settings.data.bar.position) {
|
||||
case "top":
|
||||
return (Style.barHeight + Style.marginS) * root.scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * root.scaling : 0)
|
||||
default:
|
||||
return Style.marginL * root.scaling
|
||||
}
|
||||
}
|
||||
|
||||
color: Color.transparent
|
||||
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
exclusionMode: PanelWindow.ExclusionMode.Ignore
|
||||
|
||||
Rectangle {
|
||||
id: osdItem
|
||||
|
||||
width: parent.width
|
||||
height: Math.round(contentLayout.implicitHeight + Style.marginL * 2 * root.scaling)
|
||||
radius: Style.radiusL * root.scaling
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(2, Style.borderM * root.scaling)
|
||||
visible: false
|
||||
opacity: 0
|
||||
scale: 0.85
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
id: opacityAnimation
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
id: scaleAnimation
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: 2000
|
||||
onTriggered: osdItem.hide()
|
||||
}
|
||||
|
||||
// Timer to handle visibility after animations complete
|
||||
Timer {
|
||||
id: visibilityTimer
|
||||
interval: Style.animationNormal + 50 // Add small buffer
|
||||
onTriggered: {
|
||||
osdItem.visible = false
|
||||
root.currentOSDType = ""
|
||||
// Deactivate the loader when done
|
||||
root.active = false
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: contentLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM * root.scaling
|
||||
spacing: Style.marginM * root.scaling
|
||||
|
||||
NIcon {
|
||||
icon: root.getIcon()
|
||||
color: root.getIconColor()
|
||||
font.pointSize: Style.fontSizeXL * root.scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
// Smooth icon transitions
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: Style.marginXS * root.scaling
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.round(6 * root.scaling)
|
||||
radius: Math.round(3 * root.scaling)
|
||||
color: Color.mSurfaceVariant
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width * Math.min(1.0, root.getCurrentValue())
|
||||
radius: parent.radius
|
||||
color: root.getProgressColor()
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: root.getDisplayPercentage()
|
||||
color: Color.mOnSurface
|
||||
font.pointSize: Style.fontSizeS * root.scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.minimumWidth: Math.round(32 * root.scaling)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
// Cancel any pending hide operations
|
||||
hideTimer.stop()
|
||||
visibilityTimer.stop()
|
||||
|
||||
// Make visible and animate in
|
||||
osdItem.visible = true
|
||||
// Use Qt.callLater to ensure the visible change is processed before animation
|
||||
Qt.callLater(function () {
|
||||
osdItem.opacity = 1
|
||||
osdItem.scale = 1.0
|
||||
})
|
||||
|
||||
// Start the auto-hide timer
|
||||
hideTimer.start()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
hideTimer.stop()
|
||||
visibilityTimer.stop()
|
||||
|
||||
// Start fade out animation
|
||||
osdItem.opacity = 0
|
||||
osdItem.scale = 0.85 // Less dramatic scale change for smoother effect
|
||||
|
||||
// Delay hiding the element until after animation completes
|
||||
visibilityTimer.start()
|
||||
}
|
||||
|
||||
function hideImmediately() {
|
||||
hideTimer.stop()
|
||||
visibilityTimer.stop()
|
||||
osdItem.opacity = 0
|
||||
osdItem.scale = 0.85
|
||||
osdItem.visible = false
|
||||
root.currentOSDType = ""
|
||||
root.active = false
|
||||
}
|
||||
}
|
||||
|
||||
function showOSD() {
|
||||
osdItem.show()
|
||||
}
|
||||
}
|
||||
|
||||
// Volume change monitoring
|
||||
Connections {
|
||||
target: AudioService
|
||||
|
||||
function onVolumeChanged() {
|
||||
if (volumeInitialized) {
|
||||
showOSD("volume")
|
||||
}
|
||||
}
|
||||
|
||||
function onMutedChanged() {
|
||||
if (muteInitialized) {
|
||||
showOSD("volume")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timer to initialize volume/mute flags after services are ready
|
||||
Timer {
|
||||
id: initTimer
|
||||
interval: 100 // 100ms delay to allow services to initialize
|
||||
running: true
|
||||
onTriggered: {
|
||||
volumeInitialized = true
|
||||
muteInitialized = true
|
||||
}
|
||||
}
|
||||
|
||||
// Brightness change monitoring
|
||||
Connections {
|
||||
target: BrightnessService
|
||||
|
||||
function onMonitorsChanged() {
|
||||
connectBrightnessMonitors()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
connectBrightnessMonitors()
|
||||
}
|
||||
|
||||
function connectBrightnessMonitors() {
|
||||
for (var i = 0; i < BrightnessService.monitors.length; i++) {
|
||||
let monitor = BrightnessService.monitors[i]
|
||||
// Disconnect first to avoid duplicate connections
|
||||
monitor.brightnessUpdated.disconnect(onBrightnessChanged)
|
||||
monitor.brightnessUpdated.connect(onBrightnessChanged)
|
||||
}
|
||||
}
|
||||
|
||||
function onBrightnessChanged(newBrightness) {
|
||||
if (!brightnessInitialized) {
|
||||
brightnessInitialized = true
|
||||
} else {
|
||||
showOSD("brightness")
|
||||
}
|
||||
}
|
||||
|
||||
function showOSD(type) {
|
||||
// Check if OSD is enabled in settings and can show on this screen
|
||||
if (!Settings.data.notifications.enableOSD || !canShowOnThisScreen) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update the current OSD type
|
||||
currentOSDType = type
|
||||
|
||||
// Activate the loader if not already active
|
||||
if (!root.active) {
|
||||
root.active = true
|
||||
}
|
||||
|
||||
// Show the OSD (may need to wait for loader to create the item)
|
||||
if (root.item) {
|
||||
root.item.showOSD()
|
||||
} else {
|
||||
// If item not ready yet, wait for it
|
||||
Qt.callLater(function () {
|
||||
if (root.item) {
|
||||
root.item.showOSD()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function hideOSD() {
|
||||
if (root.item && root.item.osdItem) {
|
||||
root.item.osdItem.hideImmediately()
|
||||
} else if (root.active) {
|
||||
// If loader is active but item isn't ready, just deactivate
|
||||
root.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,27 +30,27 @@ NPanel {
|
||||
readonly property var powerOptions: [{
|
||||
"action": "lock",
|
||||
"icon": "lock",
|
||||
"title": "Lock",
|
||||
"subtitle": "Lock your session"
|
||||
"title": I18n.tr("session-menu.lock"),
|
||||
"subtitle": I18n.tr("session-menu.lock-subtitle")
|
||||
}, {
|
||||
"action": "suspend",
|
||||
"icon": "suspend",
|
||||
"title": "Suspend",
|
||||
"title": I18n.tr("session-menu.suspend"),
|
||||
"subtitle": "Put the system to sleep"
|
||||
}, {
|
||||
"action": "reboot",
|
||||
"icon": "reboot",
|
||||
"title": "Reboot",
|
||||
"title": I18n.tr("session-menu.reboot"),
|
||||
"subtitle": "Restart the system"
|
||||
}, {
|
||||
"action": "logout",
|
||||
"icon": "logout",
|
||||
"title": "Logout",
|
||||
"subtitle": "End your session"
|
||||
"title": I18n.tr("session-menu.logout"),
|
||||
"subtitle": I18n.tr("session-menu.end-subtitle")
|
||||
}, {
|
||||
"action": "shutdown",
|
||||
"icon": "shutdown",
|
||||
"title": "Shutdown",
|
||||
"title": I18n.tr("session-menu.shutdown"),
|
||||
"subtitle": "Turn off the system",
|
||||
"isShutdown": true
|
||||
}]
|
||||
@@ -263,7 +263,10 @@ NPanel {
|
||||
Layout.preferredHeight: Style.baseWidgetSize * 0.8 * scaling
|
||||
|
||||
NText {
|
||||
text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil(timeRemaining / 1000)} seconds...` : "Power Menu"
|
||||
text: timerActive ? I18n.tr("session-menu.action-in-seconds", {
|
||||
"action": pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1),
|
||||
"seconds": Math.ceil(timeRemaining / 1000)
|
||||
}) : I18n.tr("session-menu.title")
|
||||
font.weight: Style.fontWeightBold
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: timerActive ? Color.mPrimary : Color.mOnSurface
|
||||
@@ -277,7 +280,7 @@ NPanel {
|
||||
|
||||
NIconButton {
|
||||
icon: timerActive ? "stop" : "close"
|
||||
tooltipText: timerActive ? "Cancel Timer" : "Close"
|
||||
tooltipText: timerActive ? I18n.tr("tooltips.cancel-timer") : I18n.tr("tooltips.close")
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
colorBg: timerActive ? Qt.alpha(Color.mError, 0.08) : Color.transparent
|
||||
colorFg: timerActive ? Color.mError : Color.mOnSurface
|
||||
@@ -419,7 +422,7 @@ NPanel {
|
||||
NText {
|
||||
text: {
|
||||
if (buttonRoot.pending) {
|
||||
return "Click again to execute immediately"
|
||||
return I18n.tr("session-menu.click-again")
|
||||
}
|
||||
return buttonRoot.subtitle
|
||||
}
|
||||
+65
-3
@@ -20,6 +20,7 @@ NBox {
|
||||
signal removeWidget(string section, int index)
|
||||
signal reorderWidget(string section, int fromIndex, int toIndex)
|
||||
signal updateWidgetSettings(string section, int index, var settings)
|
||||
signal moveWidget(string fromSection, int index, string toSection)
|
||||
signal dragPotentialStarted
|
||||
signal dragPotentialEnded
|
||||
|
||||
@@ -78,16 +79,29 @@ NBox {
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
NComboBox {
|
||||
NSearchableComboBox {
|
||||
id: comboBox
|
||||
model: availableWidgets
|
||||
label: ""
|
||||
description: ""
|
||||
placeholder: "Select a widget to add..."
|
||||
placeholder: I18n.tr("bar.widget-settings.section-editor.placeholder")
|
||||
searchPlaceholder: I18n.tr("bar.widget-settings.section-editor.search-placeholder")
|
||||
onSelected: key => comboBox.currentKey = key
|
||||
popupHeight: 340 * scaling
|
||||
minimumWidth: 200 * scaling
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
// Re-filter when the model count changes (when widgets are loaded)
|
||||
Connections {
|
||||
target: availableWidgets
|
||||
function onCountChanged() {
|
||||
// Trigger a re-filter by clearing and re-setting the search text
|
||||
var currentSearch = comboBox.searchText
|
||||
comboBox.searchText = ""
|
||||
comboBox.searchText = currentSearch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
@@ -98,7 +112,7 @@ NBox {
|
||||
colorBgHover: Color.mSecondary
|
||||
colorFgHover: Color.mOnSecondary
|
||||
enabled: comboBox.currentKey !== ""
|
||||
tooltipText: "Add widget to section"
|
||||
tooltipText: I18n.tr("tooltips.add-widget")
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: Style.marginS * scaling
|
||||
onClicked: {
|
||||
@@ -125,6 +139,7 @@ NBox {
|
||||
|
||||
Repeater {
|
||||
model: widgetModel
|
||||
|
||||
delegate: Rectangle {
|
||||
id: widgetItem
|
||||
required property int index
|
||||
@@ -158,6 +173,51 @@ NBox {
|
||||
}
|
||||
}
|
||||
|
||||
// Context menu for moving widget to other sections
|
||||
NContextMenu {
|
||||
id: contextMenu
|
||||
parent: Overlay.overlay
|
||||
width: 240 * scaling
|
||||
model: [{
|
||||
"label": I18n.tr("tooltips.move-to-left-section"),
|
||||
"action": "left",
|
||||
"icon": "arrow-bar-to-left",
|
||||
"visible": root.sectionId !== "left"
|
||||
}, {
|
||||
"label": I18n.tr("tooltips.move-to-center-section"),
|
||||
"action": "center",
|
||||
"icon": "layout-columns",
|
||||
"visible": root.sectionId !== "center"
|
||||
}, {
|
||||
"label": I18n.tr("tooltips.move-to-right-section"),
|
||||
"action": "right",
|
||||
"icon": "arrow-bar-to-right",
|
||||
"visible": root.sectionId !== "right"
|
||||
}]
|
||||
|
||||
onTriggered: action => root.moveWidget(root.sectionId, index, action)
|
||||
}
|
||||
|
||||
// Update the MouseArea to use the new context menu
|
||||
MouseArea {
|
||||
id: contextMouseArea
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
z: -1 // Below the buttons but above background
|
||||
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
// Check if click is not on the buttons area
|
||||
const localX = mouse.x
|
||||
const buttonsStartX = parent.width - (parent.buttonsCount * parent.buttonsWidth)
|
||||
|
||||
if (localX < buttonsStartX) {
|
||||
// Use the helper function to open at mouse position
|
||||
contextMenu.openAtItem(widgetItem, mouse.x, mouse.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
id: widgetContent
|
||||
anchors.centerIn: parent
|
||||
@@ -180,6 +240,7 @@ NBox {
|
||||
active: BarWidgetRegistry.widgetHasUserSettings(modelData.id)
|
||||
sourceComponent: NIconButton {
|
||||
icon: "settings"
|
||||
tooltipText: I18n.tr("tooltips.widget-settings")
|
||||
baseSize: miniButtonSize
|
||||
colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight)
|
||||
colorBg: Color.mOnSurface
|
||||
@@ -220,6 +281,7 @@ NBox {
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("tooltips.remove-widget")
|
||||
baseSize: miniButtonSize
|
||||
colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight)
|
||||
colorBg: Color.mOnSurface
|
||||
+61
-46
@@ -5,70 +5,55 @@ import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
import "./WidgetSettings" as WidgetSettings
|
||||
|
||||
// Widget Settings Dialog Component
|
||||
Popup {
|
||||
id: settingsPopup
|
||||
// Don't replace by root!
|
||||
id: widgetSettings
|
||||
|
||||
property int widgetIndex: -1
|
||||
property var widgetData: null
|
||||
property string widgetId: ""
|
||||
|
||||
property bool isMasked: false
|
||||
|
||||
// Center popup in parent
|
||||
x: (parent.width - width) * 0.5
|
||||
y: (parent.height - height) * 0.5
|
||||
|
||||
width: 500 * scaling
|
||||
width: Math.max(content.implicitWidth + padding * 2, 500 * scaling)
|
||||
height: content.implicitHeight + padding * 2
|
||||
padding: Style.marginXL * scaling
|
||||
modal: true
|
||||
|
||||
background: Rectangle {
|
||||
id: bgRect
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL * scaling
|
||||
border.color: Color.mPrimary
|
||||
border.width: Style.borderM * scaling
|
||||
}
|
||||
|
||||
// Load settings when popup opens with data
|
||||
onOpened: {
|
||||
// Mark this popup has opened in the PanelService
|
||||
PanelService.willOpenPopup(widgetSettings)
|
||||
|
||||
// Load settings when popup opens with data
|
||||
if (widgetData && widgetId) {
|
||||
loadWidgetSettings()
|
||||
}
|
||||
}
|
||||
|
||||
function loadWidgetSettings() {
|
||||
const widgetSettingsMap = {
|
||||
"ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml",
|
||||
"Battery": "WidgetSettings/BatterySettings.qml",
|
||||
"Brightness": "WidgetSettings/BrightnessSettings.qml",
|
||||
"Clock": "WidgetSettings/ClockSettings.qml",
|
||||
"CustomButton": "WidgetSettings/CustomButtonSettings.qml",
|
||||
"KeyboardLayout": "WidgetSettings/KeyboardLayoutSettings.qml",
|
||||
"MediaMini": "WidgetSettings/MediaMiniSettings.qml",
|
||||
"Microphone": "WidgetSettings/MicrophoneSettings.qml",
|
||||
"NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml",
|
||||
"Workspace": "WidgetSettings/WorkspaceSettings.qml",
|
||||
"SidePanelToggle": "WidgetSettings/SidePanelToggleSettings.qml",
|
||||
"Spacer": "WidgetSettings/SpacerSettings.qml",
|
||||
"SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml",
|
||||
"Volume": "WidgetSettings/VolumeSettings.qml"
|
||||
}
|
||||
|
||||
const source = widgetSettingsMap[widgetId]
|
||||
if (source) {
|
||||
// Use setSource to pass properties at creation time
|
||||
settingsLoader.setSource(source, {
|
||||
"widgetData": widgetData,
|
||||
"widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
})
|
||||
}
|
||||
onClosed: {
|
||||
PanelService.willClosePopup(widgetSettings)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
background: Rectangle {
|
||||
id: bgRect
|
||||
|
||||
opacity: widgetSettings.isMasked ? 0 : 1.0
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL * scaling
|
||||
border.color: Color.mPrimary
|
||||
border.width: Math.max(1, Style.borderM * scaling)
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: content
|
||||
|
||||
opacity: widgetSettings.isMasked ? 0 : 1.0
|
||||
width: parent.width
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
@@ -77,7 +62,9 @@ Popup {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: `${settingsPopup.widgetId} Settings`
|
||||
text: I18n.tr("system.widget-settings-title", {
|
||||
"widget": widgetSettings.widgetId
|
||||
})
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
@@ -86,7 +73,7 @@ Popup {
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
onClicked: settingsPopup.close()
|
||||
onClicked: widgetSettings.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,22 +102,50 @@ Popup {
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: "Cancel"
|
||||
text: I18n.tr("bar.widget-settings.dialog.cancel")
|
||||
outlined: true
|
||||
onClicked: settingsPopup.close()
|
||||
onClicked: widgetSettings.close()
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: "Apply"
|
||||
text: I18n.tr("bar.widget-settings.dialog.apply")
|
||||
icon: "check"
|
||||
onClicked: {
|
||||
if (settingsLoader.item && settingsLoader.item.saveSettings) {
|
||||
var newSettings = settingsLoader.item.saveSettings()
|
||||
root.updateWidgetSettings(sectionId, settingsPopup.widgetIndex, newSettings)
|
||||
settingsPopup.close()
|
||||
root.updateWidgetSettings(sectionId, widgetSettings.widgetIndex, newSettings)
|
||||
widgetSettings.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadWidgetSettings() {
|
||||
const widgetSettingsMap = {
|
||||
"ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml",
|
||||
"Battery": "WidgetSettings/BatterySettings.qml",
|
||||
"Brightness": "WidgetSettings/BrightnessSettings.qml",
|
||||
"Clock": "WidgetSettings/ClockSettings.qml",
|
||||
"ControlCenter": "WidgetSettings/ControlCenterSettings.qml",
|
||||
"CustomButton": "WidgetSettings/CustomButtonSettings.qml",
|
||||
"KeyboardLayout": "WidgetSettings/KeyboardLayoutSettings.qml",
|
||||
"MediaMini": "WidgetSettings/MediaMiniSettings.qml",
|
||||
"Microphone": "WidgetSettings/MicrophoneSettings.qml",
|
||||
"NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml",
|
||||
"Spacer": "WidgetSettings/SpacerSettings.qml",
|
||||
"SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml",
|
||||
"Volume": "WidgetSettings/VolumeSettings.qml",
|
||||
"Workspace": "WidgetSettings/WorkspaceSettings.qml"
|
||||
}
|
||||
|
||||
const source = widgetSettingsMap[widgetId]
|
||||
if (source) {
|
||||
// Use setSource to pass properties at creation time
|
||||
settingsLoader.setSource(source, {
|
||||
"widgetData": widgetData,
|
||||
"widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local state
|
||||
property bool valueShowIcon: widgetData.showIcon !== undefined ? widgetData.showIcon : widgetMetadata.showIcon
|
||||
property bool valueAutoHide: widgetData.autoHide !== undefined ? widgetData.autoHide : widgetMetadata.autoHide
|
||||
property string valueScrollingMode: widgetData.scrollingMode || widgetMetadata.scrollingMode
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.autoHide = valueAutoHide
|
||||
settings.showIcon = valueShowIcon
|
||||
settings.scrollingMode = valueScrollingMode
|
||||
console.log(JSON.stringify(settings))
|
||||
return settings
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.active-window.auto-hide")
|
||||
checked: root.valueAutoHide
|
||||
onToggled: checked => root.valueAutoHide = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.active-window.show-app-icon")
|
||||
checked: root.valueShowIcon
|
||||
onToggled: checked => root.valueShowIcon = checked
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("bar.widget-settings.active-window.scrolling-mode")
|
||||
model: [{
|
||||
"key": "always",
|
||||
"name": I18n.tr("options.scrolling-modes.always")
|
||||
}, {
|
||||
"key": "hover",
|
||||
"name": I18n.tr("options.scrolling-modes.hover")
|
||||
}, {
|
||||
"key": "never",
|
||||
"name": I18n.tr("options.scrolling-modes.never")
|
||||
}]
|
||||
currentKey: valueScrollingMode
|
||||
onSelected: key => valueScrollingMode = key
|
||||
minimumWidth: 200 * scaling
|
||||
}
|
||||
}
|
||||
+14
-18
@@ -25,30 +25,26 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: "Display mode"
|
||||
description: "Choose how you'd like this value to appear."
|
||||
label: I18n.tr("bar.widget-settings.battery.display-mode.label")
|
||||
description: I18n.tr("bar.widget-settings.battery.display-mode.description")
|
||||
minimumWidth: 134 * scaling
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "onhover"
|
||||
name: "On Hover"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysShow"
|
||||
name: "Always Show"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysHide"
|
||||
name: "Always Hide"
|
||||
}
|
||||
}
|
||||
model: [{
|
||||
"key": "onhover",
|
||||
"name": I18n.tr("options.display-mode.on-hover")
|
||||
}, {
|
||||
"key": "alwaysShow",
|
||||
"name": I18n.tr("options.display-mode.always-show")
|
||||
}, {
|
||||
"key": "alwaysHide",
|
||||
"name": I18n.tr("options.display-mode.always-hide")
|
||||
}]
|
||||
currentKey: root.valueDisplayMode
|
||||
onSelected: key => root.valueDisplayMode = key
|
||||
}
|
||||
|
||||
NSpinBox {
|
||||
label: "Low battery warning threshold"
|
||||
description: "Show a warning when battery falls below this percentage."
|
||||
label: I18n.tr("bar.widget-settings.battery.low-battery-threshold.label")
|
||||
description: I18n.tr("bar.widget-settings.battery.low-battery-threshold.description")
|
||||
value: valueWarningThreshold
|
||||
suffix: "%"
|
||||
minimum: 5
|
||||
+12
-16
@@ -23,23 +23,19 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: "Display mode"
|
||||
description: "Choose how you'd like this value to appear."
|
||||
label: I18n.tr("bar.widget-settings.brightness.display-mode.label")
|
||||
description: I18n.tr("bar.widget-settings.brightness.display-mode.description")
|
||||
minimumWidth: 134 * scaling
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "onhover"
|
||||
name: "On Hover"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysShow"
|
||||
name: "Always Show"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysHide"
|
||||
name: "Always Hide"
|
||||
}
|
||||
}
|
||||
model: [{
|
||||
"key": "onhover",
|
||||
"name": I18n.tr("options.display-mode.on-hover")
|
||||
}, {
|
||||
"key": "alwaysShow",
|
||||
"name": I18n.tr("options.display-mode.always-show")
|
||||
}, {
|
||||
"key": "alwaysHide",
|
||||
"name": I18n.tr("options.display-mode.always-hide")
|
||||
}]
|
||||
currentKey: valueDisplayMode
|
||||
onSelected: key => valueDisplayMode = key
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
width: 700 * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local state
|
||||
property bool valueUsePrimaryColor: widgetData.usePrimaryColor !== undefined ? widgetData.usePrimaryColor : widgetMetadata.usePrimaryColor
|
||||
property bool valueUseMonospacedFont: widgetData.useMonospacedFont !== undefined ? widgetData.useMonospacedFont : widgetMetadata.useMonospacedFont
|
||||
property string valueFormatHorizontal: widgetData.formatHorizontal !== undefined ? widgetData.formatHorizontal : widgetMetadata.formatHorizontal
|
||||
property string valueFormatVertical: widgetData.formatVertical !== undefined ? widgetData.formatVertical : widgetMetadata.formatVertical
|
||||
|
||||
// Track the currently focused input field
|
||||
property var focusedInput: null
|
||||
property int focusedLineIndex: -1
|
||||
|
||||
readonly property var now: Time.date
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.usePrimaryColor = valueUsePrimaryColor
|
||||
settings.useMonospacedFont = valueUseMonospacedFont
|
||||
settings.formatHorizontal = valueFormatHorizontal.trim()
|
||||
settings.formatVertical = valueFormatVertical.trim()
|
||||
return settings
|
||||
}
|
||||
|
||||
// Function to insert token at cursor position in the focused input
|
||||
function insertToken(token) {
|
||||
if (!focusedInput || !focusedInput.inputItem) {
|
||||
// If no input is focused, default to horiz
|
||||
if (inputHoriz.inputItem) {
|
||||
inputHoriz.inputItem.focus = true
|
||||
focusedInput = inputHoriz
|
||||
}
|
||||
}
|
||||
|
||||
if (focusedInput && focusedInput.inputItem) {
|
||||
var input = focusedInput.inputItem
|
||||
var cursorPos = input.cursorPosition
|
||||
var currentText = input.text
|
||||
|
||||
// Insert token at cursor position
|
||||
var newText = currentText.substring(0, cursorPos) + token + currentText.substring(cursorPos)
|
||||
input.text = newText + " "
|
||||
|
||||
// Move cursor after the inserted token
|
||||
input.cursorPosition = cursorPos + token.length + 1
|
||||
|
||||
// Ensure the input keeps focus
|
||||
input.focus = true
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.clock.use-primary-color.label")
|
||||
description: I18n.tr("bar.widget-settings.clock.use-primary-color.description")
|
||||
checked: valueUsePrimaryColor
|
||||
onToggled: checked => valueUsePrimaryColor = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.clock.use-monospaced-font.label")
|
||||
description: I18n.tr("bar.widget-settings.clock.use-monospaced-font.description")
|
||||
checked: valueUseMonospacedFont
|
||||
onToggled: checked => valueUseMonospacedFont = checked
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NHeader {
|
||||
label: I18n.tr("bar.widget-settings.clock.clock-display.label")
|
||||
description: I18n.tr("bar.widget-settings.clock.clock-display.description")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: main
|
||||
|
||||
spacing: Style.marginL * scaling
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1 // Equal sizing hint
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
|
||||
NTextInput {
|
||||
id: inputHoriz
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.clock.horizontal-bar.label")
|
||||
description: I18n.tr("bar.widget-settings.clock.horizontal-bar.description")
|
||||
placeholderText: "HH:mm ddd, MMM dd"
|
||||
text: valueFormatHorizontal
|
||||
onTextChanged: valueFormatHorizontal = text
|
||||
Component.onCompleted: {
|
||||
if (inputItem) {
|
||||
inputItem.onActiveFocusChanged.connect(function () {
|
||||
if (inputItem.activeFocus) {
|
||||
root.focusedInput = inputHoriz
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: inputVert
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.clock.vertical-bar.label")
|
||||
description: I18n.tr("bar.widget-settings.clock.vertical-bar.description")
|
||||
// Tokens are Qt format tokens and must not be localized
|
||||
placeholderText: "HH mm dd MM"
|
||||
text: valueFormatVertical
|
||||
onTextChanged: valueFormatVertical = text
|
||||
Component.onCompleted: {
|
||||
if (inputItem) {
|
||||
inputItem.onActiveFocusChanged.connect(function () {
|
||||
if (inputItem.activeFocus) {
|
||||
root.focusedInput = inputVert
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------
|
||||
// Preview
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
Layout.fillWidth: false
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("bar.widget-settings.clock.preview")
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 320 * scaling
|
||||
Layout.preferredHeight: 160 * scaling // Fixed height instead of fillHeight
|
||||
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusM * scaling
|
||||
border.color: Color.mSecondary
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM * scaling
|
||||
anchors.centerIn: parent
|
||||
|
||||
ColumnLayout {
|
||||
spacing: -2 * scaling
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
// Horizontal
|
||||
Repeater {
|
||||
Layout.topMargin: Style.marginM * scaling
|
||||
model: Qt.formatDateTime(now, valueFormatHorizontal.trim()).split("\\n")
|
||||
delegate: NText {
|
||||
visible: text !== ""
|
||||
text: modelData
|
||||
font.family: valueUseMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: valueUsePrimaryColor ? Color.mPrimary : Color.mOnSurface
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Vertical
|
||||
ColumnLayout {
|
||||
spacing: -2 * scaling
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Repeater {
|
||||
Layout.topMargin: Style.marginM * scaling
|
||||
model: Qt.formatDateTime(now, valueFormatVertical.trim()).split(" ")
|
||||
delegate: NText {
|
||||
visible: text !== ""
|
||||
text: modelData
|
||||
font.family: valueUseMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: valueUsePrimaryColor ? Color.mPrimary : Color.mOnSurface
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.topMargin: Style.marginM * scaling
|
||||
Layout.bottomMargin: Style.marginM * scaling
|
||||
}
|
||||
|
||||
NDateTimeTokens {
|
||||
Layout.fillWidth: true
|
||||
height: 200 * scaling
|
||||
|
||||
// Connect to token clicked signal if NDateTimeTokens provides it
|
||||
onTokenClicked: token => root.insertToken(token)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local state
|
||||
property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon
|
||||
property bool valueUseDistroLogo: widgetData.useDistroLogo !== undefined ? widgetData.useDistroLogo : widgetMetadata.useDistroLogo
|
||||
property string valueCustomIconPath: widgetData.customIconPath !== undefined ? widgetData.customIconPath : ""
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.icon = valueIcon
|
||||
settings.useDistroLogo = valueUseDistroLogo
|
||||
settings.customIconPath = valueCustomIconPath
|
||||
return settings
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.widget-settings.control-center.use-distro-logo")
|
||||
checked: valueUseDistroLogo
|
||||
onToggled: {
|
||||
valueUseDistroLogo = checked
|
||||
if (checked) {
|
||||
valueCustomIconPath = ""
|
||||
valueIcon = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("bar.widget-settings.control-center.icon.label")
|
||||
description: I18n.tr("bar.widget-settings.control-center.icon.description")
|
||||
}
|
||||
|
||||
NImageCircled {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
imagePath: valueCustomIconPath
|
||||
visible: valueCustomIconPath !== ""
|
||||
width: Style.fontSizeXL * 2 * scaling
|
||||
height: Style.fontSizeXL * 2 * scaling
|
||||
}
|
||||
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: valueIcon
|
||||
font.pointSize: Style.fontSizeXXL * 1.5 * scaling
|
||||
visible: valueIcon !== "" && valueCustomIconPath === ""
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM * scaling
|
||||
NButton {
|
||||
enabled: !valueUseDistroLogo
|
||||
text: I18n.tr("bar.widget-settings.control-center.browse-library")
|
||||
onClicked: iconPicker.open()
|
||||
}
|
||||
|
||||
NButton {
|
||||
enabled: !valueUseDistroLogo
|
||||
text: I18n.tr("bar.widget-settings.control-center.browse-file")
|
||||
onClicked: filePicker.open()
|
||||
}
|
||||
}
|
||||
|
||||
NIconPicker {
|
||||
id: iconPicker
|
||||
initialIcon: valueIcon
|
||||
onIconSelected: iconName => {
|
||||
valueIcon = iconName
|
||||
valueCustomIconPath = ""
|
||||
}
|
||||
}
|
||||
|
||||
NFilePicker {
|
||||
id: filePicker
|
||||
title: I18n.tr("bar.widget-settings.control-center.select-custom-icon")
|
||||
onAccepted: paths => valueCustomIconPath = paths[0]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.icon = valueIcon
|
||||
settings.leftClickExec = leftClickExecInput.text
|
||||
settings.rightClickExec = rightClickExecInput.text
|
||||
settings.middleClickExec = middleClickExecInput.text
|
||||
settings.textCommand = textCommandInput.text
|
||||
settings.textIntervalMs = parseInt(textIntervalInput.text || textIntervalInput.placeholderText, 10)
|
||||
return settings
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("bar.widget-settings.custom-button.icon.label")
|
||||
description: I18n.tr("bar.widget-settings.custom-button.icon.description")
|
||||
}
|
||||
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon: valueIcon
|
||||
font.pointSize: Style.fontSizeXL * scaling
|
||||
visible: valueIcon !== ""
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("bar.widget-settings.custom-button.browse")
|
||||
onClicked: iconPicker.open()
|
||||
}
|
||||
}
|
||||
|
||||
NIconPicker {
|
||||
id: iconPicker
|
||||
initialIcon: valueIcon
|
||||
onIconSelected: function (iconName) {
|
||||
valueIcon = iconName
|
||||
}
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: leftClickExecInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.custom-button.left-click")
|
||||
placeholderText: I18n.tr("placeholders.enter-command")
|
||||
text: widgetData?.leftClickExec || widgetMetadata.leftClickExec
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: rightClickExecInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.custom-button.right-click")
|
||||
placeholderText: I18n.tr("placeholders.enter-command")
|
||||
text: widgetData?.rightClickExec || widgetMetadata.rightClickExec
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: middleClickExecInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.custom-button.middle-click")
|
||||
placeholderText: I18n.tr("placeholders.enter-command")
|
||||
text: widgetData.middleClickExec || widgetMetadata.middleClickExec
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NHeader {
|
||||
label: I18n.tr("bar.widget-settings.custom-button.dynamic-text")
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: textCommandInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.custom-button.display-command-output.label")
|
||||
description: I18n.tr("bar.widget-settings.custom-button.display-command-output.description")
|
||||
placeholderText: I18n.tr("placeholders.command-example")
|
||||
text: widgetData?.textCommand || widgetMetadata.textCommand
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
id: textIntervalInput
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.custom-button.refresh-interval.label")
|
||||
description: I18n.tr("bar.widget-settings.custom-button.refresh-interval.description")
|
||||
placeholderText: String(widgetMetadata.textIntervalMs || 3000)
|
||||
text: widgetData && widgetData.textIntervalMs !== undefined ? String(widgetData.textIntervalMs) : ""
|
||||
}
|
||||
}
|
||||
+12
-16
@@ -23,23 +23,19 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: "Display mode"
|
||||
description: "Choose how you'd like this value to appear."
|
||||
label: I18n.tr("bar.widget-settings.keyboard-layout.display-mode.label")
|
||||
description: I18n.tr("bar.widget-settings.keyboard-layout.display-mode.description")
|
||||
minimumWidth: 134 * scaling
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "onhover"
|
||||
name: "On Hover"
|
||||
}
|
||||
ListElement {
|
||||
key: "forceOpen"
|
||||
name: "Force Open"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysHide"
|
||||
name: "Always Hide"
|
||||
}
|
||||
}
|
||||
model: [{
|
||||
"key": "onhover",
|
||||
"name": I18n.tr("options.display-mode.on-hover")
|
||||
}, {
|
||||
"key": "forceOpen",
|
||||
"name": I18n.tr("options.display-mode.force-open")
|
||||
}, {
|
||||
"key": "alwaysHide",
|
||||
"name": I18n.tr("options.display-mode.always-hide")
|
||||
}]
|
||||
currentKey: valueDisplayMode
|
||||
onSelected: key => valueDisplayMode = key
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Properties to receive data from parent
|
||||
property var widgetData: null
|
||||
property var widgetMetadata: null
|
||||
|
||||
// Local state
|
||||
property bool valueAutoHide: widgetData.autoHide !== undefined ? widgetData.autoHide : widgetMetadata.autoHide
|
||||
property bool valueShowAlbumArt: widgetData.showAlbumArt !== undefined ? widgetData.showAlbumArt : widgetMetadata.showAlbumArt
|
||||
property bool valueShowVisualizer: widgetData.showVisualizer !== undefined ? widgetData.showVisualizer : widgetMetadata.showVisualizer
|
||||
property string valueVisualizerType: widgetData.visualizerType || widgetMetadata.visualizerType
|
||||
property string valueScrollingMode: widgetData.scrollingMode || widgetMetadata.scrollingMode
|
||||
|
||||
function saveSettings() {
|
||||
var settings = Object.assign({}, widgetData || {})
|
||||
settings.autoHide = valueAutoHide
|
||||
settings.showAlbumArt = valueShowAlbumArt
|
||||
settings.showVisualizer = valueShowVisualizer
|
||||
settings.visualizerType = valueVisualizerType
|
||||
settings.scrollingMode = valueScrollingMode
|
||||
return settings
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("bar.widget-settings.media-mini.auto-hide")
|
||||
checked: root.valueAutoHide
|
||||
onToggled: checked => root.valueAutoHide = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.widget-settings.media-mini.show-album-art")
|
||||
checked: valueShowAlbumArt
|
||||
onToggled: checked => valueShowAlbumArt = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("bar.widget-settings.media-mini.show-visualizer")
|
||||
checked: valueShowVisualizer
|
||||
onToggled: checked => valueShowVisualizer = checked
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
visible: valueShowVisualizer
|
||||
label: I18n.tr("bar.widget-settings.media-mini.visualizer-type")
|
||||
model: [{
|
||||
"key": "linear",
|
||||
"name": I18n.tr("options.visualizer-types.linear")
|
||||
}, {
|
||||
"key": "mirrored",
|
||||
"name": I18n.tr("options.visualizer-types.mirrored")
|
||||
}, {
|
||||
"key": "wave",
|
||||
"name": I18n.tr("options.visualizer-types.wave")
|
||||
}]
|
||||
currentKey: valueVisualizerType
|
||||
onSelected: key => valueVisualizerType = key
|
||||
minimumWidth: 200 * scaling
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("bar.widget-settings.media-mini.scrolling-mode")
|
||||
model: [{
|
||||
"key": "always",
|
||||
"name": I18n.tr("options.scrolling-modes.always")
|
||||
}, {
|
||||
"key": "hover",
|
||||
"name": I18n.tr("options.scrolling-modes.hover")
|
||||
}, {
|
||||
"key": "never",
|
||||
"name": I18n.tr("options.scrolling-modes.never")
|
||||
}]
|
||||
currentKey: valueScrollingMode
|
||||
onSelected: key => valueScrollingMode = key
|
||||
minimumWidth: 200 * scaling
|
||||
}
|
||||
}
|
||||
+12
-16
@@ -23,23 +23,19 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: "Display mode"
|
||||
description: "Choose how you'd like this value to appear."
|
||||
label: I18n.tr("bar.widget-settings.microphone.display-mode.label")
|
||||
description: I18n.tr("bar.widget-settings.microphone.display-mode.description")
|
||||
minimumWidth: 134 * scaling
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "onhover"
|
||||
name: "On Hover"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysShow"
|
||||
name: "Always Show"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysHide"
|
||||
name: "Always Hide"
|
||||
}
|
||||
}
|
||||
model: [{
|
||||
"key": "onhover",
|
||||
"name": I18n.tr("options.display-mode.on-hover")
|
||||
}, {
|
||||
"key": "alwaysShow",
|
||||
"name": I18n.tr("options.display-mode.always-show")
|
||||
}, {
|
||||
"key": "alwaysHide",
|
||||
"name": I18n.tr("options.display-mode.always-hide")
|
||||
}]
|
||||
currentKey: valueDisplayMode
|
||||
onSelected: key => valueDisplayMode = key
|
||||
}
|
||||
+2
-2
@@ -25,13 +25,13 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Show unread badge"
|
||||
label: I18n.tr("bar.widget-settings.notification-history.show-unread-badge")
|
||||
checked: valueShowUnreadBadge
|
||||
onToggled: checked => valueShowUnreadBadge = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Hide badge when zero"
|
||||
label: I18n.tr("bar.widget-settings.notification-history.hide-badge-when-zero")
|
||||
checked: valueHideWhenZero
|
||||
onToggled: checked => valueHideWhenZero = checked
|
||||
}
|
||||
+3
-3
@@ -22,9 +22,9 @@ ColumnLayout {
|
||||
NTextInput {
|
||||
id: widthInput
|
||||
Layout.fillWidth: true
|
||||
label: "Width"
|
||||
description: "Spacing width in pixels"
|
||||
label: I18n.tr("bar.widget-settings.spacer.width.label")
|
||||
description: I18n.tr("bar.widget-settings.spacer.width.description")
|
||||
text: widgetData.width || widgetMetadata.width
|
||||
placeholderText: "Enter width in pixels"
|
||||
placeholderText: I18n.tr("placeholders.enter-width-pixels")
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -35,7 +35,7 @@ ColumnLayout {
|
||||
NToggle {
|
||||
id: showCpuUsage
|
||||
Layout.fillWidth: true
|
||||
label: "CPU usage"
|
||||
label: I18n.tr("bar.widget-settings.system-monitor.cpu-usage")
|
||||
checked: valueShowCpuUsage
|
||||
onToggled: checked => valueShowCpuUsage = checked
|
||||
}
|
||||
@@ -43,7 +43,7 @@ ColumnLayout {
|
||||
NToggle {
|
||||
id: showCpuTemp
|
||||
Layout.fillWidth: true
|
||||
label: "CPU temperature"
|
||||
label: I18n.tr("bar.widget-settings.system-monitor.cpu-temperature")
|
||||
checked: valueShowCpuTemp
|
||||
onToggled: checked => valueShowCpuTemp = checked
|
||||
}
|
||||
@@ -51,7 +51,7 @@ ColumnLayout {
|
||||
NToggle {
|
||||
id: showMemoryUsage
|
||||
Layout.fillWidth: true
|
||||
label: "Memory usage"
|
||||
label: I18n.tr("bar.widget-settings.system-monitor.memory-usage")
|
||||
checked: valueShowMemoryUsage
|
||||
onToggled: checked => valueShowMemoryUsage = checked
|
||||
}
|
||||
@@ -59,7 +59,7 @@ ColumnLayout {
|
||||
NToggle {
|
||||
id: showMemoryAsPercent
|
||||
Layout.fillWidth: true
|
||||
label: "Memory as percentage"
|
||||
label: I18n.tr("bar.widget-settings.system-monitor.memory-percentage")
|
||||
checked: valueShowMemoryAsPercent
|
||||
onToggled: checked => valueShowMemoryAsPercent = checked
|
||||
}
|
||||
@@ -67,7 +67,7 @@ ColumnLayout {
|
||||
NToggle {
|
||||
id: showNetworkStats
|
||||
Layout.fillWidth: true
|
||||
label: "Network traffic"
|
||||
label: I18n.tr("bar.widget-settings.system-monitor.network-traffic")
|
||||
checked: valueShowNetworkStats
|
||||
onToggled: checked => valueShowNetworkStats = checked
|
||||
}
|
||||
@@ -75,7 +75,7 @@ ColumnLayout {
|
||||
NToggle {
|
||||
id: showDiskUsage
|
||||
Layout.fillWidth: true
|
||||
label: "Storage usage"
|
||||
label: I18n.tr("bar.widget-settings.system-monitor.storage-usage")
|
||||
checked: valueShowDiskUsage
|
||||
onToggled: checked => valueShowDiskUsage = checked
|
||||
}
|
||||
+12
-16
@@ -23,23 +23,19 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: "Display mode"
|
||||
description: "Choose how you'd like this value to appear."
|
||||
label: I18n.tr("bar.widget-settings.volume.display-mode.label")
|
||||
description: I18n.tr("bar.widget-settings.volume.display-mode.description")
|
||||
minimumWidth: 134 * scaling
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "onhover"
|
||||
name: "On Hover"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysShow"
|
||||
name: "Always Show"
|
||||
}
|
||||
ListElement {
|
||||
key: "alwaysHide"
|
||||
name: "Always Hide"
|
||||
}
|
||||
}
|
||||
model: [{
|
||||
"key": "onhover",
|
||||
"name": I18n.tr("options.display-mode.on-hover")
|
||||
}, {
|
||||
"key": "alwaysShow",
|
||||
"name": I18n.tr("options.display-mode.always-show")
|
||||
}, {
|
||||
"key": "alwaysHide",
|
||||
"name": I18n.tr("options.display-mode.always-hide")
|
||||
}]
|
||||
currentKey: valueDisplayMode
|
||||
onSelected: key => valueDisplayMode = key
|
||||
}
|
||||
+13
-17
@@ -23,21 +23,17 @@ ColumnLayout {
|
||||
NComboBox {
|
||||
id: labelModeCombo
|
||||
|
||||
label: "Label Mode"
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "none"
|
||||
name: "None"
|
||||
}
|
||||
ListElement {
|
||||
key: "index"
|
||||
name: "Index"
|
||||
}
|
||||
ListElement {
|
||||
key: "name"
|
||||
name: "Name"
|
||||
}
|
||||
}
|
||||
label: I18n.tr("bar.widget-settings.workspace.label-mode")
|
||||
model: [{
|
||||
"key": "none",
|
||||
"name": I18n.tr("options.workspace-labels.none")
|
||||
}, {
|
||||
"key": "index",
|
||||
"name": I18n.tr("options.workspace-labels.index")
|
||||
}, {
|
||||
"key": "name",
|
||||
"name": I18n.tr("options.workspace-labels.name")
|
||||
}]
|
||||
currentKey: widgetData.labelMode || widgetMetadata.labelMode
|
||||
onSelected: key => labelModeCombo.currentKey = key
|
||||
minimumWidth: 200 * scaling
|
||||
@@ -45,8 +41,8 @@ ColumnLayout {
|
||||
|
||||
NToggle {
|
||||
id: hideUnoccupiedToggle
|
||||
label: "Hide unoccupied"
|
||||
description: "Don't display workspaces without windows."
|
||||
label: I18n.tr("bar.widget-settings.workspace.hide-unoccupied.label")
|
||||
description: I18n.tr("bar.widget-settings.workspace.hide-unoccupied.description")
|
||||
checked: widgetData.hideUnoccupied
|
||||
onToggled: checked => hideUnoccupiedToggle.checked = checked
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Modules.SettingsPanel.Tabs as Tabs
|
||||
import qs.Modules.Settings.Tabs
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -21,7 +21,7 @@ NPanel {
|
||||
|
||||
panelKeyboardFocus: true
|
||||
|
||||
draggable: true
|
||||
draggable: !PanelService.hasOpenedPopup
|
||||
|
||||
// Tabs enumeration, order is NOT relevant
|
||||
enum Tab {
|
||||
@@ -52,131 +52,131 @@ NPanel {
|
||||
|
||||
Component {
|
||||
id: generalTab
|
||||
Tabs.GeneralTab {}
|
||||
GeneralTab {}
|
||||
}
|
||||
Component {
|
||||
id: launcherTab
|
||||
Tabs.LauncherTab {}
|
||||
LauncherTab {}
|
||||
}
|
||||
Component {
|
||||
id: barTab
|
||||
Tabs.BarTab {}
|
||||
BarTab {}
|
||||
}
|
||||
Component {
|
||||
id: audioTab
|
||||
Tabs.AudioTab {}
|
||||
AudioTab {}
|
||||
}
|
||||
Component {
|
||||
id: displayTab
|
||||
Tabs.DisplayTab {}
|
||||
DisplayTab {}
|
||||
}
|
||||
Component {
|
||||
id: networkTab
|
||||
Tabs.NetworkTab {}
|
||||
NetworkTab {}
|
||||
}
|
||||
Component {
|
||||
id: locationTab
|
||||
Tabs.LocationTab {}
|
||||
LocationTab {}
|
||||
}
|
||||
Component {
|
||||
id: colorSchemeTab
|
||||
Tabs.ColorSchemeTab {}
|
||||
ColorSchemeTab {}
|
||||
}
|
||||
Component {
|
||||
id: wallpaperTab
|
||||
Tabs.WallpaperTab {}
|
||||
WallpaperTab {}
|
||||
}
|
||||
Component {
|
||||
id: screenRecorderTab
|
||||
Tabs.ScreenRecorderTab {}
|
||||
ScreenRecorderTab {}
|
||||
}
|
||||
Component {
|
||||
id: aboutTab
|
||||
Tabs.AboutTab {}
|
||||
AboutTab {}
|
||||
}
|
||||
Component {
|
||||
id: hooksTab
|
||||
Tabs.HooksTab {}
|
||||
HooksTab {}
|
||||
}
|
||||
Component {
|
||||
id: dockTab
|
||||
Tabs.DockTab {}
|
||||
DockTab {}
|
||||
}
|
||||
Component {
|
||||
id: notificationsTab
|
||||
Tabs.NotificationsTab {}
|
||||
NotificationsTab {}
|
||||
}
|
||||
|
||||
// Order *DOES* matter
|
||||
function updateTabsModel() {
|
||||
let newTabs = [{
|
||||
"id": SettingsPanel.Tab.General,
|
||||
"label": "General",
|
||||
"label": "settings.general.title",
|
||||
"icon": "settings-general",
|
||||
"source": generalTab
|
||||
}, {
|
||||
"id": SettingsPanel.Tab.Bar,
|
||||
"label": "Bar",
|
||||
"label": "settings.bar.title",
|
||||
"icon": "settings-bar",
|
||||
"source": barTab
|
||||
}, {
|
||||
"id": SettingsPanel.Tab.Dock,
|
||||
"label": "Dock",
|
||||
"label": "settings.dock.title",
|
||||
"icon": "settings-dock",
|
||||
"source": dockTab
|
||||
}, {
|
||||
"id": SettingsPanel.Tab.Launcher,
|
||||
"label": "Launcher",
|
||||
"label": "settings.launcher.title",
|
||||
"icon": "settings-launcher",
|
||||
"source": launcherTab
|
||||
}, {
|
||||
"id": SettingsPanel.Tab.Audio,
|
||||
"label": "Audio",
|
||||
"label": "settings.audio.title",
|
||||
"icon": "settings-audio",
|
||||
"source": audioTab
|
||||
}, {
|
||||
"id": SettingsPanel.Tab.Display,
|
||||
"label": "Display",
|
||||
"label": "settings.display.title",
|
||||
"icon": "settings-display",
|
||||
"source": displayTab
|
||||
}, {
|
||||
"id": SettingsPanel.Tab.Notifications,
|
||||
"label": "Notifications",
|
||||
"label": "settings.notifications.title",
|
||||
"icon": "settings-notifications",
|
||||
"source": notificationsTab
|
||||
}, {
|
||||
"id": SettingsPanel.Tab.Network,
|
||||
"label": "Network",
|
||||
"label": "settings.network.title",
|
||||
"icon": "settings-network",
|
||||
"source": networkTab
|
||||
}, {
|
||||
"id": SettingsPanel.Tab.Location,
|
||||
"label": "Location",
|
||||
"label": "settings.location.title",
|
||||
"icon": "settings-location",
|
||||
"source": locationTab
|
||||
}, {
|
||||
"id": SettingsPanel.Tab.ColorScheme,
|
||||
"label": "Color Scheme",
|
||||
"label": "settings.color-scheme.title",
|
||||
"icon": "settings-color-scheme",
|
||||
"source": colorSchemeTab
|
||||
}, {
|
||||
"id": SettingsPanel.Tab.Wallpaper,
|
||||
"label": "Wallpaper",
|
||||
"label": "settings.wallpaper.title",
|
||||
"icon": "settings-wallpaper",
|
||||
"source": wallpaperTab
|
||||
}, {
|
||||
"id": SettingsPanel.Tab.ScreenRecorder,
|
||||
"label": "Screen Recorder",
|
||||
"label": "settings.screen-recorder.title",
|
||||
"icon": "settings-screen-recorder",
|
||||
"source": screenRecorderTab
|
||||
}, {
|
||||
"id": SettingsPanel.Tab.Hooks,
|
||||
"label": "Hooks",
|
||||
"label": "settings.hooks.title",
|
||||
"icon": "settings-hooks",
|
||||
"source": hooksTab
|
||||
}, {
|
||||
"id": SettingsPanel.Tab.About,
|
||||
"label": "About",
|
||||
"label": "settings.about.title",
|
||||
"icon": "settings-about",
|
||||
"source": aboutTab
|
||||
}]
|
||||
@@ -391,7 +391,7 @@ NPanel {
|
||||
|
||||
// Tab label
|
||||
NText {
|
||||
text: modelData.label
|
||||
text: I18n.tr(modelData.label)
|
||||
color: tabTextColor
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
@@ -451,7 +451,7 @@ NPanel {
|
||||
|
||||
// Main title
|
||||
NText {
|
||||
text: root.tabsModel[currentTabIndex]?.label || ""
|
||||
text: I18n.tr(root.tabsModel[currentTabIndex]?.label) || ""
|
||||
font.pointSize: Style.fontSizeXL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
@@ -462,7 +462,7 @@ NPanel {
|
||||
// Close button
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close."
|
||||
tooltipText: I18n.tr("tooltips.close")
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onClicked: root.close()
|
||||
}
|
||||
@@ -17,8 +17,8 @@ ColumnLayout {
|
||||
property var contributors: GitHubService.contributors
|
||||
|
||||
NHeader {
|
||||
label: "Noctalia shell"
|
||||
description: "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell."
|
||||
label: I18n.tr("settings.about.noctalia.section.label")
|
||||
description: I18n.tr("settings.about.noctalia.section.description")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
@@ -31,7 +31,7 @@ ColumnLayout {
|
||||
columnSpacing: Style.marginS * scaling
|
||||
|
||||
NText {
|
||||
text: "Latest version:"
|
||||
text: I18n.tr("settings.about.noctalia.latest-version")
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Installed version:"
|
||||
text: I18n.tr("settings.about.noctalia.installed-version")
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ ColumnLayout {
|
||||
|
||||
NText {
|
||||
id: updateText
|
||||
text: "Download latest release"
|
||||
text: I18n.tr("settings.about.noctalia.download-latest")
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary
|
||||
}
|
||||
@@ -123,8 +123,12 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NHeader {
|
||||
label: "Contributors"
|
||||
description: `Shout-out to our ${root.contributors.length} <b>awesome</b> contributors!`
|
||||
label: I18n.tr("settings.about.contributors.section.label")
|
||||
description: root.contributors.length === 1 ? I18n.tr("settings.about.contributors.section.description", {
|
||||
"count": root.contributors.length
|
||||
}) : I18n.tr("settings.about.contributors.section.description_plural", {
|
||||
"count": root.contributors.length
|
||||
})
|
||||
}
|
||||
|
||||
GridView {
|
||||
@@ -11,8 +11,8 @@ ColumnLayout {
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
NHeader {
|
||||
label: "Volumes"
|
||||
description: "Adjust volume controls and audio levels."
|
||||
label: I18n.tr("settings.audio.volumes.section.label")
|
||||
description: I18n.tr("settings.audio.volumes.section.description")
|
||||
}
|
||||
|
||||
property real localVolume: AudioService.volume
|
||||
@@ -30,15 +30,15 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: "Output volume"
|
||||
description: "System-wide volume level."
|
||||
label: I18n.tr("settings.audio.volumes.output-volume.label")
|
||||
description: I18n.tr("settings.audio.volumes.output-volume.description")
|
||||
}
|
||||
|
||||
// Pipewire seems a bit finicky, if we spam too many volume changes it breaks easily
|
||||
// Probably because they have some quick fades in and out to avoid clipping
|
||||
// We use a timer to space out the updates, to avoid lock up
|
||||
Timer {
|
||||
interval: Style.animationFast
|
||||
interval: 100
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
@@ -51,13 +51,11 @@ ColumnLayout {
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: Settings.data.audio.volumeOverdrive ? 2.0 : 1.0
|
||||
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
|
||||
value: localVolume
|
||||
stepSize: 0.01
|
||||
text: Math.floor(AudioService.volume * 100) + "%"
|
||||
onMoved: {
|
||||
localVolume = value
|
||||
}
|
||||
text: Math.round(AudioService.volume * 100) + "%"
|
||||
onMoved: value => localVolume = value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,8 +65,8 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NToggle {
|
||||
label: "Mute audio output"
|
||||
description: "Mute the system's main audio output."
|
||||
label: I18n.tr("settings.audio.volumes.mute-output.label")
|
||||
description: I18n.tr("settings.audio.volumes.mute-output.description")
|
||||
checked: AudioService.muted
|
||||
onToggled: checked => {
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
@@ -84,17 +82,17 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: "Input volume"
|
||||
description: "Microphone input volume level."
|
||||
label: I18n.tr("settings.audio.volumes.input-volume.label")
|
||||
description: I18n.tr("settings.audio.volumes.input-volume.description")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 1.0
|
||||
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
|
||||
value: AudioService.inputVolume
|
||||
stepSize: 0.01
|
||||
text: Math.floor(AudioService.inputVolume * 100) + "%"
|
||||
text: Math.round(AudioService.inputVolume * 100) + "%"
|
||||
onMoved: value => AudioService.setInputVolume(value)
|
||||
}
|
||||
}
|
||||
@@ -105,8 +103,8 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NToggle {
|
||||
label: "Mute audio input"
|
||||
description: "Mute the default audio input (microphone)."
|
||||
label: I18n.tr("settings.audio.volumes.mute-input.label")
|
||||
description: I18n.tr("settings.audio.volumes.mute-input.description")
|
||||
checked: AudioService.inputMuted
|
||||
onToggled: checked => AudioService.setInputMuted(checked)
|
||||
}
|
||||
@@ -119,8 +117,8 @@ ColumnLayout {
|
||||
|
||||
NSpinBox {
|
||||
Layout.fillWidth: true
|
||||
label: "Volume step size"
|
||||
description: "Adjust the step size for volume changes (scroll wheel, keyboard shortcuts)."
|
||||
label: I18n.tr("settings.audio.volumes.step-size.label")
|
||||
description: I18n.tr("settings.audio.volumes.step-size.description")
|
||||
minimum: 1
|
||||
maximum: 25
|
||||
value: Settings.data.audio.volumeStep
|
||||
@@ -130,6 +128,19 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
|
||||
// Raise maximum volume above 100%
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("settings.audio.volumes.volume-overdrive.label")
|
||||
description: I18n.tr("settings.audio.volumes.volume-overdrive.description")
|
||||
checked: Settings.data.audio.volumeOverdrive
|
||||
onToggled: checked => Settings.data.audio.volumeOverdrive = checked
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginXL * scaling
|
||||
@@ -141,8 +152,8 @@ ColumnLayout {
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NHeader {
|
||||
label: "Audio devices"
|
||||
description: "Choose your audio input and output devices."
|
||||
label: I18n.tr("settings.audio.devices.section.label")
|
||||
description: I18n.tr("settings.audio.devices.section.description")
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
@@ -157,8 +168,8 @@ ColumnLayout {
|
||||
Layout.bottomMargin: Style.marginL * scaling
|
||||
|
||||
NLabel {
|
||||
label: "Output device"
|
||||
description: "Select the desired audio output device."
|
||||
label: I18n.tr("settings.audio.devices.output-device.label")
|
||||
description: I18n.tr("settings.audio.devices.output-device.description")
|
||||
}
|
||||
|
||||
Repeater {
|
||||
@@ -167,7 +178,10 @@ ColumnLayout {
|
||||
required property PwNode modelData
|
||||
ButtonGroup.group: sinks
|
||||
checked: AudioService.sink?.id === modelData.id
|
||||
onClicked: AudioService.setAudioSink(modelData)
|
||||
onClicked: {
|
||||
AudioService.setAudioSink(modelData)
|
||||
localVolume = AudioService.volume
|
||||
}
|
||||
text: modelData.description
|
||||
}
|
||||
}
|
||||
@@ -184,8 +198,8 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: "Input device"
|
||||
description: "Select the desired audio input device."
|
||||
label: I18n.tr("settings.audio.devices.input-device.label")
|
||||
description: I18n.tr("settings.audio.devices.input-device.description")
|
||||
}
|
||||
|
||||
Repeater {
|
||||
@@ -213,15 +227,15 @@ ColumnLayout {
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
NHeader {
|
||||
label: "Media players"
|
||||
description: "Set your preferred and ignored media applications."
|
||||
label: I18n.tr("settings.audio.media.section.label")
|
||||
description: I18n.tr("settings.audio.media.section.description")
|
||||
}
|
||||
|
||||
// Preferred player
|
||||
NTextInput {
|
||||
label: "Primary player"
|
||||
description: "Enter a keyword to identify your main player."
|
||||
placeholderText: "e.g. spotify, vlc, mpv"
|
||||
label: I18n.tr("settings.audio.media.primary-player.label")
|
||||
description: I18n.tr("settings.audio.media.primary-player.description")
|
||||
placeholderText: I18n.tr("settings.audio.media.primary-player.placeholder")
|
||||
text: Settings.data.audio.preferredPlayer
|
||||
onTextChanged: {
|
||||
Settings.data.audio.preferredPlayer = text
|
||||
@@ -240,9 +254,9 @@ ColumnLayout {
|
||||
|
||||
NTextInput {
|
||||
id: blacklistInput
|
||||
label: "Excluded player"
|
||||
description: "Add keywords for players you want the system to ignore. Each keyword should be on a new line."
|
||||
placeholderText: "type substring and press +"
|
||||
label: I18n.tr("settings.audio.media.excluded-player.label")
|
||||
description: I18n.tr("settings.audio.media.excluded-player.description")
|
||||
placeholderText: I18n.tr("settings.audio.media.excluded-player.placeholder")
|
||||
}
|
||||
|
||||
// Button aligned to the center of the actual input field
|
||||
@@ -321,90 +335,72 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Divider
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginXL * scaling
|
||||
Layout.bottomMargin: Style.marginXL * scaling
|
||||
}
|
||||
|
||||
// AudioService Visualizer Category
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NHeader {
|
||||
label: "Audio visualizer"
|
||||
description: "Customize visual effects that respond to audio playback."
|
||||
}
|
||||
|
||||
// AudioService Visualizer section
|
||||
NComboBox {
|
||||
id: audioVisualizerCombo
|
||||
label: "Visualization type"
|
||||
description: "Choose a visualization type for media playback"
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "none"
|
||||
name: "None"
|
||||
}
|
||||
ListElement {
|
||||
key: "linear"
|
||||
name: "Linear"
|
||||
}
|
||||
ListElement {
|
||||
key: "mirrored"
|
||||
name: "Mirrored"
|
||||
}
|
||||
ListElement {
|
||||
key: "wave"
|
||||
name: "Wave"
|
||||
}
|
||||
}
|
||||
label: I18n.tr("settings.audio.media.visualizer-type.label")
|
||||
description: I18n.tr("settings.audio.media.visualizer-type.description")
|
||||
model: [{
|
||||
"key": "none",
|
||||
"name": I18n.tr("options.visualizer-types.none")
|
||||
}, {
|
||||
"key": "linear",
|
||||
"name": I18n.tr("options.visualizer-types.linear")
|
||||
}, {
|
||||
"key": "mirrored",
|
||||
"name": I18n.tr("options.visualizer-types.mirrored")
|
||||
}, {
|
||||
"key": "wave",
|
||||
"name": I18n.tr("options.visualizer-types.wave")
|
||||
}]
|
||||
currentKey: Settings.data.audio.visualizerType
|
||||
onSelected: key => Settings.data.audio.visualizerType = key
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: "Frame rate"
|
||||
description: "Higher rates are smoother but use more resources."
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "30"
|
||||
name: "30 FPS"
|
||||
}
|
||||
ListElement {
|
||||
key: "60"
|
||||
name: "60 FPS"
|
||||
}
|
||||
ListElement {
|
||||
key: "100"
|
||||
name: "100 FPS"
|
||||
}
|
||||
ListElement {
|
||||
key: "120"
|
||||
name: "120 FPS"
|
||||
}
|
||||
ListElement {
|
||||
key: "144"
|
||||
name: "144 FPS"
|
||||
}
|
||||
ListElement {
|
||||
key: "165"
|
||||
name: "165 FPS"
|
||||
}
|
||||
ListElement {
|
||||
key: "240"
|
||||
name: "240 FPS"
|
||||
}
|
||||
}
|
||||
label: I18n.tr("settings.audio.media.frame-rate.label")
|
||||
description: I18n.tr("settings.audio.media.frame-rate.description")
|
||||
model: [{
|
||||
"key": "30",
|
||||
"name": I18n.tr("options.frame-rates.fps", {
|
||||
"fps": "30"
|
||||
})
|
||||
}, {
|
||||
"key": "60",
|
||||
"name": I18n.tr("options.frame-rates.fps", {
|
||||
"fps": "60"
|
||||
})
|
||||
}, {
|
||||
"key": "100",
|
||||
"name": I18n.tr("options.frame-rates.fps", {
|
||||
"fps": "100"
|
||||
})
|
||||
}, {
|
||||
"key": "120",
|
||||
"name": I18n.tr("options.frame-rates.fps", {
|
||||
"fps": "120"
|
||||
})
|
||||
}, {
|
||||
"key": "144",
|
||||
"name": I18n.tr("options.frame-rates.fps", {
|
||||
"fps": "144"
|
||||
})
|
||||
}, {
|
||||
"key": "165",
|
||||
"name": I18n.tr("options.frame-rates.fps", {
|
||||
"fps": "165"
|
||||
})
|
||||
}, {
|
||||
"key": "240",
|
||||
"name": I18n.tr("options.frame-rates.fps", {
|
||||
"fps": "240"
|
||||
})
|
||||
}]
|
||||
currentKey: Settings.data.audio.cavaFrameRate
|
||||
onSelected: key => Settings.data.audio.cavaFrameRate = key
|
||||
}
|
||||
}
|
||||
// Divider
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginXL * scaling
|
||||
@@ -5,7 +5,7 @@ import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.SettingsPanel.Bar
|
||||
import qs.Modules.Settings.Bar
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
@@ -41,54 +41,45 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NHeader {
|
||||
label: "Appearance"
|
||||
description: "Customize the bar's appearance and position."
|
||||
label: I18n.tr("settings.bar.appearance.section.label")
|
||||
description: I18n.tr("settings.bar.appearance.section.description")
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: "Bar position"
|
||||
description: "Choose where to place the bar on the screen."
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "top"
|
||||
name: "Top"
|
||||
}
|
||||
ListElement {
|
||||
key: "bottom"
|
||||
name: "Bottom"
|
||||
}
|
||||
ListElement {
|
||||
key: "left"
|
||||
name: "Left"
|
||||
}
|
||||
ListElement {
|
||||
key: "right"
|
||||
name: "Right"
|
||||
}
|
||||
}
|
||||
label: I18n.tr("settings.bar.appearance.position.label")
|
||||
description: I18n.tr("settings.bar.appearance.position.description")
|
||||
model: [{
|
||||
"key": "top",
|
||||
"name": I18n.tr("options.bar.position.top")
|
||||
}, {
|
||||
"key": "bottom",
|
||||
"name": I18n.tr("options.bar.position.bottom")
|
||||
}, {
|
||||
"key": "left",
|
||||
"name": I18n.tr("options.bar.position.left")
|
||||
}, {
|
||||
"key": "right",
|
||||
"name": I18n.tr("options.bar.position.right")
|
||||
}]
|
||||
currentKey: Settings.data.bar.position
|
||||
onSelected: key => Settings.data.bar.position = key
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: "Bar density"
|
||||
description: "Adjust the bar's padding for a compact or spacious look."
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "compact"
|
||||
name: "Compact"
|
||||
}
|
||||
ListElement {
|
||||
key: "default"
|
||||
name: "Default"
|
||||
}
|
||||
ListElement {
|
||||
key: "comfortable"
|
||||
name: "Comfortable"
|
||||
}
|
||||
}
|
||||
label: I18n.tr("settings.bar.appearance.density.label")
|
||||
description: I18n.tr("settings.bar.appearance.density.description")
|
||||
model: [{
|
||||
"key": "compact",
|
||||
"name": I18n.tr("options.bar.density.compact")
|
||||
}, {
|
||||
"key": "default",
|
||||
"name": I18n.tr("options.bar.density.default")
|
||||
}, {
|
||||
"key": "comfortable",
|
||||
"name": I18n.tr("options.bar.density.comfortable")
|
||||
}]
|
||||
currentKey: Settings.data.bar.density
|
||||
onSelected: key => Settings.data.bar.density = key
|
||||
}
|
||||
@@ -98,8 +89,8 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: "Background opacity"
|
||||
description: "Adjust the background opacity of the bar."
|
||||
label: I18n.tr("settings.bar.appearance.background-opacity.label")
|
||||
description: I18n.tr("settings.bar.appearance.background-opacity.description")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
@@ -115,16 +106,16 @@ ColumnLayout {
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: "Show capsule"
|
||||
description: "Show widget backgrounds."
|
||||
label: I18n.tr("settings.bar.appearance.show-capsule.label")
|
||||
description: I18n.tr("settings.bar.appearance.show-capsule.description")
|
||||
checked: Settings.data.bar.showCapsule
|
||||
onToggled: checked => Settings.data.bar.showCapsule = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: "Floating bar"
|
||||
description: "Displays the bar as a floating 'pill'. Note: This will move the screen corners to the edges."
|
||||
label: I18n.tr("settings.bar.appearance.floating.label")
|
||||
description: I18n.tr("settings.bar.appearance.floating.description")
|
||||
checked: Settings.data.bar.floating
|
||||
onToggled: checked => Settings.data.bar.floating = checked
|
||||
}
|
||||
@@ -136,8 +127,8 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: "Margins"
|
||||
description: "Adjust the margins around the floating bar."
|
||||
label: I18n.tr("settings.bar.appearance.margins.label")
|
||||
description: I18n.tr("settings.bar.appearance.margins.description")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
@@ -148,7 +139,7 @@ ColumnLayout {
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
NText {
|
||||
text: "Vertical"
|
||||
text: I18n.tr("settings.bar.appearance.margins.vertical")
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
@@ -168,7 +159,7 @@ ColumnLayout {
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
NText {
|
||||
text: "Horizontal"
|
||||
text: I18n.tr("settings.bar.appearance.margins.horizontal")
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
@@ -198,8 +189,8 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NHeader {
|
||||
label: "Widgets positioning"
|
||||
description: "Drag and drop widgets to reorder them within each section, or use the add/remove buttons to manage widgets."
|
||||
label: I18n.tr("settings.bar.widgets.section.label")
|
||||
description: I18n.tr("settings.bar.widgets.section.description")
|
||||
}
|
||||
|
||||
// Bar Sections
|
||||
@@ -219,6 +210,7 @@ ColumnLayout {
|
||||
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
|
||||
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
|
||||
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
|
||||
onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection)
|
||||
onDragPotentialStarted: root.handleDragStart()
|
||||
onDragPotentialEnded: root.handleDragEnd()
|
||||
}
|
||||
@@ -233,6 +225,7 @@ ColumnLayout {
|
||||
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
|
||||
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
|
||||
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
|
||||
onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection)
|
||||
onDragPotentialStarted: root.handleDragStart()
|
||||
onDragPotentialEnded: root.handleDragEnd()
|
||||
}
|
||||
@@ -247,6 +240,7 @@ ColumnLayout {
|
||||
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
|
||||
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
|
||||
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
|
||||
onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection)
|
||||
onDragPotentialStarted: root.handleDragStart()
|
||||
onDragPotentialEnded: root.handleDragEnd()
|
||||
}
|
||||
@@ -265,8 +259,8 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NHeader {
|
||||
label: "Monitor display"
|
||||
description: "Show bar on specific monitors. Defaults to all if none are chosen."
|
||||
label: I18n.tr("settings.bar.monitors.section.label")
|
||||
description: I18n.tr("settings.bar.monitors.section.description")
|
||||
}
|
||||
|
||||
Repeater {
|
||||
@@ -274,7 +268,11 @@ ColumnLayout {
|
||||
delegate: NCheckbox {
|
||||
Layout.fillWidth: true
|
||||
label: modelData.name || "Unknown"
|
||||
description: `${modelData.model} (${modelData.width}x${modelData.height})`
|
||||
description: I18n.tr("system.monitor-description", {
|
||||
"model": modelData.model,
|
||||
"width": modelData.width,
|
||||
"height": modelData.height
|
||||
})
|
||||
checked: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
@@ -341,6 +339,25 @@ ColumnLayout {
|
||||
//Logger.log("BarTab", `Updated widget settings for ${settings.id} in ${section} section`)
|
||||
}
|
||||
|
||||
function _moveWidgetBetweenSections(fromSection, index, toSection) {
|
||||
// Get the widget from the source section
|
||||
if (index >= 0 && index < Settings.data.bar.widgets[fromSection].length) {
|
||||
var widget = Settings.data.bar.widgets[fromSection][index]
|
||||
|
||||
// Remove from source section
|
||||
var sourceArray = Settings.data.bar.widgets[fromSection].slice()
|
||||
sourceArray.splice(index, 1)
|
||||
Settings.data.bar.widgets[fromSection] = sourceArray
|
||||
|
||||
// Add to target section
|
||||
var targetArray = Settings.data.bar.widgets[toSection].slice()
|
||||
targetArray.push(widget)
|
||||
Settings.data.bar.widgets[toSection] = targetArray
|
||||
|
||||
//Logger.log("BarTab", `Moved widget ${widget.id} from ${fromSection} to ${toSection}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Base list model for all combo boxes
|
||||
ListModel {
|
||||
id: availableWidgets
|
||||
+69
-32
@@ -65,10 +65,10 @@ ColumnLayout {
|
||||
// Matugen exists, enable it
|
||||
Settings.data.colorSchemes.useWallpaperColors = true
|
||||
MatugenService.generateFromWallpaper()
|
||||
ToastService.showNotice("Matugen", "Enabled")
|
||||
ToastService.showNotice(I18n.tr("settings.color-scheme.color-source.enable-matugen.label"), I18n.tr("toast.matugen.enabled"))
|
||||
} else {
|
||||
// Matugen not found
|
||||
ToastService.showWarning("Matugen", "Not installed")
|
||||
ToastService.showWarning(I18n.tr("settings.color-scheme.color-source.enable-matugen.label"), I18n.tr("toast.matugen.not-installed"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,14 +106,14 @@ ColumnLayout {
|
||||
|
||||
// Main Toggles - Dark Mode / Matugen
|
||||
NHeader {
|
||||
label: "Color Source"
|
||||
description: "Main settings for Noctalia's colors."
|
||||
label: I18n.tr("settings.color-scheme.color-source.section.label")
|
||||
description: I18n.tr("settings.color-scheme.color-source.section.description")
|
||||
}
|
||||
|
||||
// Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants)
|
||||
NToggle {
|
||||
label: "Dark mode"
|
||||
description: "Switches to a darker theme for easier viewing at night."
|
||||
label: I18n.tr("settings.color-scheme.color-source.dark-mode.label")
|
||||
description: I18n.tr("settings.color-scheme.color-source.dark-mode.description")
|
||||
checked: Settings.data.colorSchemes.darkMode
|
||||
enabled: true
|
||||
onToggled: checked => Settings.data.colorSchemes.darkMode = checked
|
||||
@@ -121,8 +121,8 @@ ColumnLayout {
|
||||
|
||||
// Use Matugen
|
||||
NToggle {
|
||||
label: "Enable Matugen"
|
||||
description: "Automatically generate colors based on your active wallpaper."
|
||||
label: I18n.tr("settings.color-scheme.color-source.enable-matugen.label")
|
||||
description: I18n.tr("settings.color-scheme.color-source.enable-matugen.description")
|
||||
checked: Settings.data.colorSchemes.useWallpaperColors
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
@@ -130,7 +130,7 @@ ColumnLayout {
|
||||
matugenCheck.running = true
|
||||
} else {
|
||||
Settings.data.colorSchemes.useWallpaperColors = false
|
||||
ToastService.showNotice("Matugen", "Disabled")
|
||||
ToastService.showNotice(I18n.tr("settings.color-scheme.color-source.enable-matugen.label"), I18n.tr("toast.matugen.disabled"))
|
||||
|
||||
if (Settings.data.colorSchemes.predefinedScheme) {
|
||||
|
||||
@@ -152,8 +152,8 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NHeader {
|
||||
label: "Predefined color schemes"
|
||||
description: "To use these color schemes, you must turn off Matugen. With Matugen enabled, colors are automatically generated from your wallpaper."
|
||||
label: I18n.tr("settings.color-scheme.predefined.section.label")
|
||||
description: I18n.tr("settings.color-scheme.predefined.section.description")
|
||||
}
|
||||
|
||||
// Color Schemes Grid
|
||||
@@ -324,16 +324,23 @@ ColumnLayout {
|
||||
visible: Settings.data.colorSchemes.useWallpaperColors
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
NHeader {
|
||||
label: I18n.tr("settings.color-scheme.matugen.section.label")
|
||||
description: I18n.tr("settings.color-scheme.matugen.section.description")
|
||||
}
|
||||
|
||||
// UI Components
|
||||
NCollapsible {
|
||||
Layout.fillWidth: true
|
||||
label: "UI"
|
||||
description: "Desktop environment and UI toolkit theming."
|
||||
label: I18n.tr("settings.color-scheme.matugen.ui.label")
|
||||
description: I18n.tr("settings.color-scheme.matugen.ui.description")
|
||||
defaultExpanded: false
|
||||
|
||||
NCheckbox {
|
||||
label: "GTK 4 (libadwaita)"
|
||||
description: "Write ~/.config/gtk-4.0/gtk.css"
|
||||
description: I18n.tr("settings.color-scheme.matugen.ui.gtk4.description", {
|
||||
"filepath": "~/.config/gtk-4.0/gtk.css"
|
||||
})
|
||||
checked: Settings.data.matugen.gtk4
|
||||
onToggled: checked => {
|
||||
Settings.data.matugen.gtk4 = checked
|
||||
@@ -344,7 +351,9 @@ ColumnLayout {
|
||||
|
||||
NCheckbox {
|
||||
label: "GTK 3"
|
||||
description: "Write ~/.config/gtk-3.0/gtk.css"
|
||||
description: I18n.tr("settings.color-scheme.matugen.ui.gtk3.description", {
|
||||
"filepath": "~/.config/gtk-3.0/gtk.css"
|
||||
})
|
||||
checked: Settings.data.matugen.gtk3
|
||||
onToggled: checked => {
|
||||
Settings.data.matugen.gtk3 = checked
|
||||
@@ -355,7 +364,9 @@ ColumnLayout {
|
||||
|
||||
NCheckbox {
|
||||
label: "Qt6ct"
|
||||
description: "Write ~/.config/qt6ct/colors/noctalia.conf"
|
||||
description: I18n.tr("settings.color-scheme.matugen.ui.qt6.description", {
|
||||
"filepath": "~/.config/qt6ct/colors/noctalia.conf"
|
||||
})
|
||||
checked: Settings.data.matugen.qt6
|
||||
onToggled: checked => {
|
||||
Settings.data.matugen.qt6 = checked
|
||||
@@ -366,7 +377,9 @@ ColumnLayout {
|
||||
|
||||
NCheckbox {
|
||||
label: "Qt5ct"
|
||||
description: "Write ~/.config/qt5ct/colors/noctalia.conf"
|
||||
description: I18n.tr("settings.color-scheme.matugen.ui.qt5.description", {
|
||||
"filepath": "~/.config/qt5ct/colors/noctalia.conf"
|
||||
})
|
||||
checked: Settings.data.matugen.qt5
|
||||
onToggled: checked => {
|
||||
Settings.data.matugen.qt5 = checked
|
||||
@@ -379,13 +392,17 @@ ColumnLayout {
|
||||
// Terminal Emulators
|
||||
NCollapsible {
|
||||
Layout.fillWidth: true
|
||||
label: "Terminal"
|
||||
description: "Terminal emulator theming."
|
||||
label: I18n.tr("settings.color-scheme.matugen.terminal.label")
|
||||
description: I18n.tr("settings.color-scheme.matugen.terminal.description")
|
||||
defaultExpanded: false
|
||||
|
||||
NCheckbox {
|
||||
label: "Kitty"
|
||||
description: ProgramCheckerService.kittyAvailable ? "Write ~/.config/kitty/themes/noctalia.conf and reload" : "Requires kitty terminal to be installed"
|
||||
description: ProgramCheckerService.kittyAvailable ? I18n.tr("settings.color-scheme.matugen.terminal.kitty.description", {
|
||||
"filepath": "~/.config/kitty/themes/noctalia.conf"
|
||||
}) : I18n.tr("settings.color-scheme.matugen.terminal.kitty.description-missing", {
|
||||
"app": "kitty"
|
||||
})
|
||||
checked: Settings.data.matugen.kitty
|
||||
enabled: ProgramCheckerService.kittyAvailable
|
||||
opacity: ProgramCheckerService.kittyAvailable ? 1.0 : 0.6
|
||||
@@ -400,7 +417,11 @@ ColumnLayout {
|
||||
|
||||
NCheckbox {
|
||||
label: "Ghostty"
|
||||
description: ProgramCheckerService.ghosttyAvailable ? "Write ~/.config/ghostty/themes/noctalia and reload" : "Requires ghostty terminal to be installed"
|
||||
description: ProgramCheckerService.ghosttyAvailable ? I18n.tr("settings.color-scheme.matugen.terminal.ghostty.description", {
|
||||
"filepath": "~/.config/ghostty/themes/noctalia"
|
||||
}) : I18n.tr("settings.color-scheme.matugen.terminal.ghostty.description-missing", {
|
||||
"app": "ghostty"
|
||||
})
|
||||
checked: Settings.data.matugen.ghostty
|
||||
enabled: ProgramCheckerService.ghosttyAvailable
|
||||
opacity: ProgramCheckerService.ghosttyAvailable ? 1.0 : 0.6
|
||||
@@ -415,7 +436,11 @@ ColumnLayout {
|
||||
|
||||
NCheckbox {
|
||||
label: "Foot"
|
||||
description: ProgramCheckerService.footAvailable ? "Write ~/.config/foot/themes/noctalia and reload" : "Requires foot terminal to be installed"
|
||||
description: ProgramCheckerService.footAvailable ? I18n.tr("settings.color-scheme.matugen.terminal.foot.description", {
|
||||
"filepath": "~/.config/foot/themes/noctalia"
|
||||
}) : I18n.tr("settings.color-scheme.matugen.terminal.foot.description-missing", {
|
||||
"app": "foot"
|
||||
})
|
||||
checked: Settings.data.matugen.foot
|
||||
enabled: ProgramCheckerService.footAvailable
|
||||
opacity: ProgramCheckerService.footAvailable ? 1.0 : 0.6
|
||||
@@ -432,13 +457,17 @@ ColumnLayout {
|
||||
// Applications
|
||||
NCollapsible {
|
||||
Layout.fillWidth: true
|
||||
label: "Programs"
|
||||
description: "Application-specific theming."
|
||||
label: I18n.tr("settings.color-scheme.matugen.programs.label")
|
||||
description: I18n.tr("settings.color-scheme.matugen.programs.description")
|
||||
defaultExpanded: false
|
||||
|
||||
NCheckbox {
|
||||
label: "Fuzzel"
|
||||
description: ProgramCheckerService.fuzzelAvailable ? "Write ~/.config/fuzzel/themes/noctalia and reload" : "Requires fuzzel launcher to be installed"
|
||||
description: ProgramCheckerService.fuzzelAvailable ? I18n.tr("settings.color-scheme.matugen.programs.fuzzel.description", {
|
||||
"filepath": "~/.config/fuzzel/themes/noctalia"
|
||||
}) : I18n.tr("settings.color-scheme.matugen.programs.fuzzel.description-missing", {
|
||||
"app": "fuzzel"
|
||||
})
|
||||
checked: Settings.data.matugen.fuzzel
|
||||
enabled: ProgramCheckerService.fuzzelAvailable
|
||||
opacity: ProgramCheckerService.fuzzelAvailable ? 1.0 : 0.6
|
||||
@@ -453,7 +482,11 @@ ColumnLayout {
|
||||
|
||||
NCheckbox {
|
||||
label: "Vesktop"
|
||||
description: ProgramCheckerService.vesktopAvailable ? "Write ~/.config/vesktop/themes/noctalia.theme.css" : "Requires vesktop Discord client to be installed"
|
||||
description: ProgramCheckerService.vesktopAvailable ? I18n.tr("settings.color-scheme.matugen.programs.vesktop.description", {
|
||||
"filepath": "~/.config/vesktop/themes/noctalia.theme.css"
|
||||
}) : I18n.tr("settings.color-scheme.matugen.programs.vesktop.description-missing", {
|
||||
"app": "vesktop"
|
||||
})
|
||||
checked: Settings.data.matugen.vesktop
|
||||
enabled: ProgramCheckerService.vesktopAvailable
|
||||
opacity: ProgramCheckerService.vesktopAvailable ? 1.0 : 0.6
|
||||
@@ -467,8 +500,12 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NCheckbox {
|
||||
label: "Pywalfox (Firefox)"
|
||||
description: ProgramCheckerService.pywalfoxAvailable ? "Write ~/.cache/wal/colors.json and run pywalfox update" : "Requires pywalfox package to be installed"
|
||||
label: "Pywalfox"
|
||||
description: ProgramCheckerService.pywalfoxAvailable ? I18n.tr("settings.color-scheme.matugen.programs.pywalfox.description", {
|
||||
"filepath": "~/.cache/wal/colors.json"
|
||||
}) : I18n.tr("settings.color-scheme.matugen.programs.pywalfox.description-missing", {
|
||||
"app": "pywalfox"
|
||||
})
|
||||
checked: Settings.data.matugen.pywalfox
|
||||
enabled: ProgramCheckerService.pywalfoxAvailable
|
||||
opacity: ProgramCheckerService.pywalfoxAvailable ? 1.0 : 0.6
|
||||
@@ -485,13 +522,13 @@ ColumnLayout {
|
||||
// Miscellaneous
|
||||
NCollapsible {
|
||||
Layout.fillWidth: true
|
||||
label: "Misc"
|
||||
description: "Additional configuration options."
|
||||
label: I18n.tr("settings.color-scheme.matugen.misc.label")
|
||||
description: I18n.tr("settings.color-scheme.matugen.misc.description")
|
||||
defaultExpanded: false
|
||||
|
||||
NCheckbox {
|
||||
label: "User Templates"
|
||||
description: "Enable user-defined Matugen config from ~/.config/matugen/config.toml"
|
||||
label: I18n.tr("settings.color-scheme.matugen.misc.user-templates.label")
|
||||
description: I18n.tr("settings.color-scheme.matugen.misc.user-templates.description")
|
||||
checked: Settings.data.matugen.enableUserTemplates
|
||||
onToggled: checked => {
|
||||
Settings.data.matugen.enableUserTemplates = checked
|
||||
@@ -38,10 +38,10 @@ ColumnLayout {
|
||||
if (exitCode === 0) {
|
||||
Settings.data.nightLight.enabled = true
|
||||
NightLightService.apply()
|
||||
ToastService.showNotice("Night Light", "Enabled")
|
||||
ToastService.showNotice(I18n.tr("settings.display.night-light.section.label"), I18n.tr("toast.night-light.enabled"))
|
||||
} else {
|
||||
Settings.data.nightLight.enabled = false
|
||||
ToastService.showWarning("Night Light", "wlsunset not installed")
|
||||
ToastService.showWarning(I18n.tr("settings.display.night-light.section.label"), I18n.tr("toast.night-light.not-installed"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ ColumnLayout {
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
NHeader {
|
||||
label: "Per-monitor settings"
|
||||
description: "Adjust scaling and brightness for each display."
|
||||
label: I18n.tr("settings.display.monitors.section.label")
|
||||
description: I18n.tr("settings.display.monitors.section.description")
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
@@ -63,7 +63,7 @@ ColumnLayout {
|
||||
model: Quickshell.screens || []
|
||||
delegate: Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
|
||||
implicitHeight: contentCol.implicitHeight + Style.marginL * 2 * scaling
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
@@ -83,13 +83,18 @@ ColumnLayout {
|
||||
|
||||
ColumnLayout {
|
||||
id: contentCol
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL * scaling
|
||||
width: parent.width - 2 * Style.marginL * scaling
|
||||
x: Style.marginL * scaling
|
||||
y: Style.marginL * scaling
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
NLabel {
|
||||
label: modelData.name || "Unknown"
|
||||
description: `${modelData.model} (${modelData.width}x${modelData.height})`
|
||||
description: I18n.tr("system.monitor-description", {
|
||||
"model": modelData.model,
|
||||
"width": modelData.width,
|
||||
"height": modelData.height
|
||||
})
|
||||
}
|
||||
|
||||
// Scale
|
||||
@@ -98,11 +103,11 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginM * scaling
|
||||
spacing: Style.marginL * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: "Scale"
|
||||
text: I18n.tr("settings.display.monitors.scale")
|
||||
Layout.preferredWidth: 80 * scaling
|
||||
}
|
||||
|
||||
@@ -113,7 +118,9 @@ ColumnLayout {
|
||||
stepSize: 0.01
|
||||
value: localScaling
|
||||
onPressedChanged: (pressed, value) => ScalingService.setScreenScale(modelData, value)
|
||||
text: `${Math.round(localScaling * 100)}%`
|
||||
text: I18n.tr("system.scaling-percentage", {
|
||||
"percentage": Math.round(localScaling * 100)
|
||||
})
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
@@ -125,7 +132,7 @@ ColumnLayout {
|
||||
NIconButton {
|
||||
icon: "refresh"
|
||||
baseSize: Style.baseWidgetSize * 0.9
|
||||
tooltipText: "Reset scaling."
|
||||
tooltipText: I18n.tr("settings.display.monitors.reset-scaling")
|
||||
onClicked: ScalingService.setScreenScale(modelData, 1.0)
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -142,10 +149,10 @@ ColumnLayout {
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
NText {
|
||||
text: "Brightness"
|
||||
text: I18n.tr("settings.display.monitors.brightness")
|
||||
Layout.preferredWidth: 80 * scaling
|
||||
}
|
||||
|
||||
@@ -179,24 +186,6 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginXL * scaling
|
||||
Layout.bottomMargin: Style.marginXL * scaling
|
||||
}
|
||||
|
||||
// Brightness Section
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NHeader {
|
||||
label: "Brightness"
|
||||
description: "Adjust brightness related settings."
|
||||
}
|
||||
|
||||
// Brightness Step Section
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS * scaling
|
||||
@@ -204,8 +193,8 @@ ColumnLayout {
|
||||
|
||||
NSpinBox {
|
||||
Layout.fillWidth: true
|
||||
label: "Brightness step size"
|
||||
description: "Adjust the step size for brightness changes (scroll wheel and keyboard shortcuts)."
|
||||
label: I18n.tr("settings.display.monitors.brightness-step.label")
|
||||
description: I18n.tr("settings.display.monitors.brightness-step.description")
|
||||
minimum: 1
|
||||
maximum: 50
|
||||
value: Settings.data.brightness.brightnessStep
|
||||
@@ -228,14 +217,14 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NHeader {
|
||||
label: "Night light"
|
||||
description: "Reduce blue light emission to help you sleep better and reduce eye strain."
|
||||
label: I18n.tr("settings.display.night-light.section.label")
|
||||
description: I18n.tr("settings.display.night-light.section.description")
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Enable night light"
|
||||
description: "Apply a warm color filter to reduce blue light emission."
|
||||
label: I18n.tr("settings.display.night-light.enable.label")
|
||||
description: I18n.tr("settings.display.night-light.enable.description")
|
||||
checked: Settings.data.nightLight.enabled
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
@@ -245,7 +234,7 @@ ColumnLayout {
|
||||
Settings.data.nightLight.enabled = false
|
||||
Settings.data.nightLight.forced = false
|
||||
NightLightService.apply()
|
||||
ToastService.showNotice("Night Light", "Disabled")
|
||||
ToastService.showNotice(I18n.tr("settings.display.night-light.section.label"), I18n.tr("toast.night-light.disabled"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -256,8 +245,8 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
NLabel {
|
||||
label: "Color temperature"
|
||||
description: "Set the color warmth for nighttime and daytime."
|
||||
label: I18n.tr("settings.display.night-light.temperature.label")
|
||||
description: I18n.tr("settings.display.night-light.temperature.description")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
@@ -268,7 +257,7 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
NText {
|
||||
text: "Night"
|
||||
text: I18n.tr("settings.display.night-light.temperature.night")
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
@@ -289,10 +278,8 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
|
||||
Item {}
|
||||
|
||||
NText {
|
||||
text: "Day"
|
||||
text: I18n.tr("settings.display.night-light.temperature.day")
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
@@ -315,31 +302,31 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Automatic Scheduling"
|
||||
description: `Based on the sunset and sunrise time in <i>${LocationService.stableName}</i> - recommended.`
|
||||
label: I18n.tr("settings.display.night-light.auto-schedule.label")
|
||||
description: I18n.tr("settings.display.night-light.auto-schedule.description", {
|
||||
"location": LocationService.stableName
|
||||
})
|
||||
checked: Settings.data.nightLight.autoSchedule
|
||||
onToggled: checked => Settings.data.nightLight.autoSchedule = checked
|
||||
visible: Settings.data.nightLight.enabled
|
||||
}
|
||||
|
||||
// Schedule settings
|
||||
// Manual scheduling
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXS * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
visible: Settings.data.nightLight.enabled && !Settings.data.nightLight.autoSchedule && !Settings.data.nightLight.forced
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("settings.display.night-light.manual-schedule.label")
|
||||
description: I18n.tr("settings.display.night-light.manual-schedule.description")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: false
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NLabel {
|
||||
label: "Manual Scheduling"
|
||||
}
|
||||
|
||||
Item {// add a little more spacing
|
||||
}
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
NText {
|
||||
text: "Sunrise Time"
|
||||
text: I18n.tr("settings.display.night-light.manual-schedule.sunrise")
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
@@ -347,23 +334,25 @@ ColumnLayout {
|
||||
NComboBox {
|
||||
model: timeOptions
|
||||
currentKey: Settings.data.nightLight.manualSunrise
|
||||
placeholder: "Select start time"
|
||||
placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-start")
|
||||
onSelected: key => Settings.data.nightLight.manualSunrise = key
|
||||
minimumWidth: 120 * scaling
|
||||
}
|
||||
|
||||
Item {// add a little more spacing
|
||||
Item {
|
||||
Layout.preferredWidth: 20 * scaling
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Sunset Time"
|
||||
text: I18n.tr("settings.display.night-light.manual-schedule.sunset")
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
model: timeOptions
|
||||
currentKey: Settings.data.nightLight.manualSunset
|
||||
placeholder: "Select stop time"
|
||||
placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-stop")
|
||||
onSelected: key => Settings.data.nightLight.manualSunset = key
|
||||
minimumWidth: 120 * scaling
|
||||
}
|
||||
@@ -372,8 +361,8 @@ ColumnLayout {
|
||||
|
||||
// Force activation toggle
|
||||
NToggle {
|
||||
label: "Force activation"
|
||||
description: "Ignores the schedule and applies the night filter immediately."
|
||||
label: I18n.tr("settings.display.night-light.force-activation.label")
|
||||
description: I18n.tr("settings.display.night-light.force-activation.description")
|
||||
checked: Settings.data.nightLight.forced
|
||||
onToggled: checked => {
|
||||
Settings.data.nightLight.forced = checked
|
||||
@@ -24,20 +24,20 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NHeader {
|
||||
label: "Appearance"
|
||||
description: "Customize the dock's behavior and appearance."
|
||||
label: I18n.tr("settings.dock.appearance.section.label")
|
||||
description: I18n.tr("settings.dock.appearance.section.description")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Auto-hide"
|
||||
description: "Automatically hide when not in use."
|
||||
label: I18n.tr("settings.dock.appearance.auto-hide.label")
|
||||
description: I18n.tr("settings.dock.appearance.auto-hide.description")
|
||||
checked: Settings.data.dock.autoHide
|
||||
onToggled: checked => Settings.data.dock.autoHide = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Exclusive zone"
|
||||
description: "Prevent window overlap."
|
||||
label: I18n.tr("settings.dock.appearance.exclusive-zone.label")
|
||||
description: I18n.tr("settings.dock.appearance.exclusive-zone.description")
|
||||
checked: Settings.data.dock.exclusive
|
||||
onToggled: checked => Settings.data.dock.exclusive = checked
|
||||
}
|
||||
@@ -46,8 +46,8 @@ ColumnLayout {
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.fillWidth: true
|
||||
NLabel {
|
||||
label: "Background opacity"
|
||||
description: "Adjust the dock's background opacity."
|
||||
label: I18n.tr("settings.dock.appearance.background-opacity.label")
|
||||
description: I18n.tr("settings.dock.appearance.background-opacity.description")
|
||||
}
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
@@ -65,8 +65,8 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: "Dock floating distance"
|
||||
description: "Adjust the floating distance from the screen edge."
|
||||
label: I18n.tr("settings.dock.appearance.floating-distance.label")
|
||||
description: I18n.tr("settings.dock.appearance.floating-distance.description")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
@@ -92,8 +92,8 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NHeader {
|
||||
label: "Monitor display"
|
||||
description: "Choose which monitor to display the dock on."
|
||||
label: I18n.tr("settings.dock.monitors.section.label")
|
||||
description: I18n.tr("settings.dock.monitors.section.description")
|
||||
}
|
||||
|
||||
Repeater {
|
||||
@@ -101,7 +101,11 @@ ColumnLayout {
|
||||
delegate: NCheckbox {
|
||||
Layout.fillWidth: true
|
||||
label: modelData.name || "Unknown"
|
||||
description: `${modelData.model} (${modelData.width}x${modelData.height})`
|
||||
description: I18n.tr("system.monitor-description", {
|
||||
"model": modelData.model,
|
||||
"width": modelData.width,
|
||||
"height": modelData.height
|
||||
})
|
||||
checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
@@ -10,8 +10,8 @@ ColumnLayout {
|
||||
id: root
|
||||
|
||||
NHeader {
|
||||
label: "Profile"
|
||||
description: "Edit your user details and avatar."
|
||||
label: I18n.tr("settings.general.profile.section.label")
|
||||
description: I18n.tr("settings.general.profile.section.description")
|
||||
}
|
||||
|
||||
// Profile section
|
||||
@@ -30,18 +30,31 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
Layout.fillWidth: true
|
||||
label: `${Quickshell.env("USER") || "user"}'s profile picture`
|
||||
description: "Your profile picture that appears throughout the interface."
|
||||
NTextInputButton {
|
||||
label: I18n.tr("settings.general.profile.picture.label", {
|
||||
"user": Quickshell.env("USER" || "User")
|
||||
})
|
||||
description: I18n.tr("settings.general.profile.picture.description")
|
||||
text: Settings.data.general.avatarImage
|
||||
placeholderText: "/home/user/.face"
|
||||
onEditingFinished: {
|
||||
Settings.data.general.avatarImage = text
|
||||
placeholderText: I18n.tr("placeholders.profile-picture-path")
|
||||
buttonIcon: "photo"
|
||||
buttonTooltip: "Browse for avatar image"
|
||||
onInputEditingFinished: Settings.data.general.avatarImage = text
|
||||
onButtonClicked: {
|
||||
filePicker.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NFilePicker {
|
||||
id: filePicker
|
||||
pickerType: "file"
|
||||
title: I18n.tr("settings.general.profile.select-avatar")
|
||||
initialPath: Settings.data.general.avatarImage.substr(0, Settings.data.general.avatarImage.lastIndexOf("/")) || Quickshell.env("HOME")
|
||||
nameFilters: ["Image files (*.jpg *.jpeg *.png *.gif *.pnm *.bmp *.face)", "All files (*)"]
|
||||
onAccepted: paths => Settings.data.general.avatarImage = paths[0]
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginXL * scaling
|
||||
@@ -54,13 +67,13 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NHeader {
|
||||
label: "User interface"
|
||||
description: "Customize the look, feel, and behavior of the interface."
|
||||
label: I18n.tr("settings.general.ui.section.label")
|
||||
description: I18n.tr("settings.general.ui.section.description")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Dim desktop"
|
||||
description: "Dim the desktop when panels or menus are open."
|
||||
label: I18n.tr("settings.general.ui.dim-desktop.label")
|
||||
description: I18n.tr("settings.general.ui.dim-desktop.description")
|
||||
checked: Settings.data.general.dimDesktop
|
||||
onToggled: checked => Settings.data.general.dimDesktop = checked
|
||||
}
|
||||
@@ -70,8 +83,8 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: "Border radius"
|
||||
description: "Controls the corner roundness of windows, buttons, and other elements."
|
||||
label: I18n.tr("settings.general.ui.border-radius.label")
|
||||
description: I18n.tr("settings.general.ui.border-radius.description")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
@@ -91,8 +104,8 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: "Animation speed"
|
||||
description: "Adjust global animation speed."
|
||||
label: I18n.tr("settings.general.ui.animation-speed.label")
|
||||
description: I18n.tr("settings.general.ui.animation-speed.description")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
@@ -119,20 +132,20 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NHeader {
|
||||
label: "Screen corners"
|
||||
description: "Customize screen corner rounding and visual effects."
|
||||
label: I18n.tr("settings.general.screen-corners.section.label")
|
||||
description: I18n.tr("settings.general.screen-corners.section.description")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Show screen corners"
|
||||
description: "Display rounded corners on the edge of the screen."
|
||||
label: I18n.tr("settings.general.screen-corners.show-corners.label")
|
||||
description: I18n.tr("settings.general.screen-corners.show-corners.description")
|
||||
checked: Settings.data.general.showScreenCorners
|
||||
onToggled: checked => Settings.data.general.showScreenCorners = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Solid black corners"
|
||||
description: "Use solid black instead of the bar background color."
|
||||
label: I18n.tr("settings.general.screen-corners.solid-black.label")
|
||||
description: I18n.tr("settings.general.screen-corners.solid-black.description")
|
||||
checked: Settings.data.general.forceBlackScreenCorners
|
||||
onToggled: checked => Settings.data.general.forceBlackScreenCorners = checked
|
||||
}
|
||||
@@ -142,8 +155,8 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: "Screen corners radius"
|
||||
description: "Adjust the rounded corners of the screen."
|
||||
label: I18n.tr("settings.general.screen-corners.radius.label")
|
||||
description: I18n.tr("settings.general.screen-corners.radius.description")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
@@ -169,8 +182,8 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NHeader {
|
||||
label: "Fonts"
|
||||
description: "Choose the fonts used throughout the interface."
|
||||
label: I18n.tr("settings.general.fonts.section.label")
|
||||
description: I18n.tr("settings.general.fonts.section.description")
|
||||
}
|
||||
|
||||
// Font configuration section
|
||||
@@ -179,12 +192,12 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NSearchableComboBox {
|
||||
label: "Default font"
|
||||
description: "Main font used throughout the interface."
|
||||
label: I18n.tr("settings.general.fonts.default.label")
|
||||
description: I18n.tr("settings.general.fonts.default.description")
|
||||
model: FontService.availableFonts
|
||||
currentKey: Settings.data.ui.fontDefault
|
||||
placeholder: "Select default font..."
|
||||
searchPlaceholder: "Search fonts..."
|
||||
placeholder: I18n.tr("settings.general.fonts.default.placeholder")
|
||||
searchPlaceholder: I18n.tr("settings.general.fonts.default.search-placeholder")
|
||||
popupHeight: 420 * scaling
|
||||
minimumWidth: 300 * scaling
|
||||
onSelected: function (key) {
|
||||
@@ -193,12 +206,12 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NSearchableComboBox {
|
||||
label: "Monospaced font"
|
||||
description: "Monospaced font used for numbers and stats display."
|
||||
label: I18n.tr("settings.general.fonts.monospace.label")
|
||||
description: I18n.tr("settings.general.fonts.monospace.description")
|
||||
model: FontService.monospaceFonts
|
||||
currentKey: Settings.data.ui.fontFixed
|
||||
placeholder: "Select monospace font..."
|
||||
searchPlaceholder: "Search monospace fonts..."
|
||||
placeholder: I18n.tr("settings.general.fonts.monospace.placeholder")
|
||||
searchPlaceholder: I18n.tr("settings.general.fonts.monospace.search-placeholder")
|
||||
popupHeight: 320 * scaling
|
||||
minimumWidth: 300 * scaling
|
||||
onSelected: function (key) {
|
||||
@@ -207,12 +220,12 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NSearchableComboBox {
|
||||
label: "Accent font"
|
||||
description: "Large font used for prominent displays."
|
||||
label: I18n.tr("settings.general.fonts.accent.label")
|
||||
description: I18n.tr("settings.general.fonts.accent.description")
|
||||
model: FontService.displayFonts
|
||||
currentKey: Settings.data.ui.fontBillboard
|
||||
placeholder: "Select display font..."
|
||||
searchPlaceholder: "Search display fonts..."
|
||||
placeholder: I18n.tr("settings.general.fonts.accent.placeholder")
|
||||
searchPlaceholder: I18n.tr("settings.general.fonts.accent.search-placeholder")
|
||||
popupHeight: 320 * scaling
|
||||
minimumWidth: 300 * scaling
|
||||
onSelected: function (key) {
|
||||
@@ -11,14 +11,14 @@ ColumnLayout {
|
||||
width: root.width
|
||||
|
||||
NHeader {
|
||||
label: "System hooks"
|
||||
description: "Configure commands to be executed when system events occur."
|
||||
label: I18n.tr("settings.hooks.system-hooks.section.label")
|
||||
description: I18n.tr("settings.hooks.system-hooks.section.description")
|
||||
}
|
||||
|
||||
// Enable/Disable Toggle
|
||||
NToggle {
|
||||
label: "Enable hooks"
|
||||
description: "Enable or disable all hook commands."
|
||||
label: I18n.tr("settings.hooks.system-hooks.enable.label")
|
||||
description: I18n.tr("settings.hooks.system-hooks.enable.description")
|
||||
checked: Settings.data.hooks.enabled
|
||||
onToggled: checked => Settings.data.hooks.enabled = checked
|
||||
}
|
||||
@@ -35,9 +35,9 @@ ColumnLayout {
|
||||
// Wallpaper Hook Section
|
||||
NInputAction {
|
||||
id: wallpaperHookInput
|
||||
label: "Wallpaper Change Hook"
|
||||
description: "Command to be executed when wallpaper changes."
|
||||
placeholderText: "e.g., notify-send \"Wallpaper\" \"Changed\""
|
||||
label: I18n.tr("settings.hooks.wallpaper-changed.label")
|
||||
description: I18n.tr("settings.hooks.wallpaper-changed.description")
|
||||
placeholderText: I18n.tr("settings.hooks.wallpaper-changed.placeholder")
|
||||
text: Settings.data.hooks.wallpaperChange
|
||||
onEditingFinished: {
|
||||
Settings.data.hooks.wallpaperChange = wallpaperHookInput.text
|
||||
@@ -57,9 +57,9 @@ ColumnLayout {
|
||||
// Dark Mode Hook Section
|
||||
NInputAction {
|
||||
id: darkModeHookInput
|
||||
label: "Theme Toggle Hook"
|
||||
description: "Command to be executed when theme toggles between dark and light mode."
|
||||
placeholderText: "e.g., notify-send \"Theme\" \"Toggled\""
|
||||
label: I18n.tr("settings.hooks.theme-changed.label")
|
||||
description: I18n.tr("settings.hooks.theme-changed.description")
|
||||
placeholderText: I18n.tr("settings.hooks.theme-changed.placeholder")
|
||||
text: Settings.data.hooks.darkModeChange
|
||||
onEditingFinished: {
|
||||
Settings.data.hooks.darkModeChange = darkModeHookInput.text
|
||||
@@ -82,13 +82,13 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: "Hook Command Information"
|
||||
description: "• Commands are executed via shell (sh -c)\n• Commands run in background (detached)\n• Test buttons execute with current values"
|
||||
label: I18n.tr("settings.hooks.info.command-info.label")
|
||||
description: I18n.tr("settings.hooks.info.command-info.description")
|
||||
}
|
||||
|
||||
NLabel {
|
||||
label: "Available Parameters"
|
||||
description: "• Wallpaper Hook: $1 = wallpaper path, $2 = screen name\n• Theme Toggle Hook: $1 = true/false (dark mode state)"
|
||||
label: I18n.tr("settings.hooks.info.parameters.label")
|
||||
description: I18n.tr("settings.hooks.info.parameters.description")
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
-42
@@ -10,45 +10,37 @@ ColumnLayout {
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
NHeader {
|
||||
label: "Appearance"
|
||||
description: "Customize the launcher's behavior and appearance."
|
||||
label: I18n.tr("settings.launcher.settings.section.label")
|
||||
description: I18n.tr("settings.launcher.settings.section.description")
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
id: launcherPosition
|
||||
label: "Position"
|
||||
description: "Choose where the Launcher panel appears."
|
||||
label: I18n.tr("settings.launcher.settings.position.label")
|
||||
description: I18n.tr("settings.launcher.settings.position.description")
|
||||
Layout.fillWidth: true
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
key: "center"
|
||||
name: "Center (default)"
|
||||
}
|
||||
ListElement {
|
||||
key: "top_left"
|
||||
name: "Top left"
|
||||
}
|
||||
ListElement {
|
||||
key: "top_right"
|
||||
name: "Top right"
|
||||
}
|
||||
ListElement {
|
||||
key: "bottom_left"
|
||||
name: "Bottom left"
|
||||
}
|
||||
ListElement {
|
||||
key: "bottom_right"
|
||||
name: "Bottom right"
|
||||
}
|
||||
ListElement {
|
||||
key: "bottom_center"
|
||||
name: "Bottom center"
|
||||
}
|
||||
ListElement {
|
||||
key: "top_center"
|
||||
name: "Top center"
|
||||
}
|
||||
}
|
||||
model: [{
|
||||
"key": "center",
|
||||
"name": I18n.tr("options.launcher.position.center")
|
||||
}, {
|
||||
"key": "top_left",
|
||||
"name": I18n.tr("options.launcher.position.top_left")
|
||||
}, {
|
||||
"key": "top_right",
|
||||
"name": I18n.tr("options.launcher.position.top_right")
|
||||
}, {
|
||||
"key": "bottom_left",
|
||||
"name": I18n.tr("options.launcher.position.bottom_left")
|
||||
}, {
|
||||
"key": "bottom_right",
|
||||
"name": I18n.tr("options.launcher.position.bottom_right")
|
||||
}, {
|
||||
"key": "bottom_center",
|
||||
"name": I18n.tr("options.launcher.position.bottom_center")
|
||||
}, {
|
||||
"key": "top_center",
|
||||
"name": I18n.tr("options.launcher.position.top_center")
|
||||
}]
|
||||
currentKey: Settings.data.appLauncher.position
|
||||
onSelected: function (key) {
|
||||
Settings.data.appLauncher.position = key
|
||||
@@ -60,14 +52,14 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: "Background opacity"
|
||||
text: I18n.tr("settings.launcher.settings.background-opacity.label")
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Adjust the background opacity of the launcher."
|
||||
text: I18n.tr("settings.launcher.settings.background-opacity.description")
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
wrapMode: Text.WordWrap
|
||||
@@ -87,22 +79,22 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Enable clipboard history"
|
||||
description: "Access previously copied items from the launcher."
|
||||
label: I18n.tr("settings.launcher.settings.clipboard-history.label")
|
||||
description: I18n.tr("settings.launcher.settings.clipboard-history.description")
|
||||
checked: Settings.data.appLauncher.enableClipboardHistory
|
||||
onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Sort by most used"
|
||||
description: "When enabled, frequently launched apps appear first in the list."
|
||||
label: I18n.tr("settings.launcher.settings.sort-by-usage.label")
|
||||
description: I18n.tr("settings.launcher.settings.sort-by-usage.description")
|
||||
checked: Settings.data.appLauncher.sortByMostUsed
|
||||
onToggled: checked => Settings.data.appLauncher.sortByMostUsed = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Use App2Unit to launch applications"
|
||||
description: "Uses an alternative launch method to better manage app processes and prevent issues."
|
||||
label: I18n.tr("settings.launcher.settings.use-app2unit.label")
|
||||
description: I18n.tr("settings.launcher.settings.use-app2unit.description")
|
||||
checked: Settings.data.appLauncher.useApp2Unit
|
||||
onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked
|
||||
}
|
||||
+21
-25
@@ -10,8 +10,8 @@ ColumnLayout {
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
NHeader {
|
||||
label: "Your location"
|
||||
description: "Get accurate weather and night light scheduling by setting your location."
|
||||
label: I18n.tr("settings.location.location.section.label")
|
||||
description: I18n.tr("settings.location.location.section.description")
|
||||
}
|
||||
|
||||
// Location section
|
||||
@@ -20,10 +20,10 @@ ColumnLayout {
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
NTextInput {
|
||||
label: "Search for a location"
|
||||
description: "e.g., Toronto, ON"
|
||||
label: I18n.tr("settings.location.location.search.label")
|
||||
description: I18n.tr("settings.location.location.search.description")
|
||||
text: Settings.data.location.name || Settings.defaultLocation
|
||||
placeholderText: "Enter the location name"
|
||||
placeholderText: I18n.tr("settings.location.location.search.placeholder")
|
||||
onEditingFinished: {
|
||||
// Verify the location has really changed to avoid extra resets
|
||||
var newLocation = text.trim()
|
||||
@@ -42,13 +42,16 @@ ColumnLayout {
|
||||
|
||||
NText {
|
||||
visible: LocationService.coordinatesReady
|
||||
text: `${LocationService.stableName} (${LocationService.displayCoordinates})`
|
||||
text: I18n.tr("system.location-display", {
|
||||
"name": LocationService.stableName,
|
||||
"coordinates": LocationService.displayCoordinates
|
||||
})
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignRight
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
Layout.bottomMargin: 12 * scaling
|
||||
Layout.bottomMargin: Style.marginM * scaling
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,13 +67,13 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
NHeader {
|
||||
label: "Weather"
|
||||
description: "Choose your preferred temperature unit."
|
||||
label: I18n.tr("settings.location.weather.section.label")
|
||||
description: I18n.tr("settings.location.weather.section.description")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Display temperature in Fahrenheit (°F)"
|
||||
description: "Display temperature in Fahrenheit instead of Celsius."
|
||||
label: I18n.tr("settings.location.weather.fahrenheit.label")
|
||||
description: I18n.tr("settings.location.weather.fahrenheit.description")
|
||||
checked: Settings.data.location.useFahrenheit
|
||||
onToggled: checked => Settings.data.location.useFahrenheit = checked
|
||||
}
|
||||
@@ -82,33 +85,26 @@ ColumnLayout {
|
||||
Layout.bottomMargin: Style.marginXL * scaling
|
||||
}
|
||||
|
||||
// Weather section
|
||||
// Date & time section
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NHeader {
|
||||
label: "Date & time"
|
||||
description: "Customize how date and time appear."
|
||||
label: I18n.tr("settings.location.date-time.section.label")
|
||||
description: I18n.tr("settings.location.date-time.section.description")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Use 12-hour time format"
|
||||
description: "On for AM/PM format (e.g., 8:00 PM), off for 24-hour format (e.g., 20:00)."
|
||||
label: I18n.tr("settings.location.date-time.12hour-format.label")
|
||||
description: I18n.tr("settings.location.date-time.12hour-format.description")
|
||||
checked: Settings.data.location.use12hourFormat
|
||||
onToggled: checked => Settings.data.location.use12hourFormat = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Show month before day"
|
||||
description: "On for 09/17/2025, off for 17/09/2025."
|
||||
checked: Settings.data.location.monthBeforeDay
|
||||
onToggled: checked => Settings.data.location.monthBeforeDay = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Show week numbers"
|
||||
description: "Displays the week of the year (e.g., Week 38) in the calendar."
|
||||
label: I18n.tr("settings.location.date-time.week-numbers.label")
|
||||
description: I18n.tr("settings.location.date-time.week-numbers.description")
|
||||
checked: Settings.data.location.showWeekNumberInCalendar
|
||||
onToggled: checked => Settings.data.location.showWeekNumberInCalendar = checked
|
||||
}
|
||||
@@ -12,17 +12,17 @@ ColumnLayout {
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
NHeader {
|
||||
label: "Manage Wi-Fi and Bluetooth connections."
|
||||
description: I18n.tr("settings.network.section.description")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Enable Wi-Fi"
|
||||
label: I18n.tr("settings.network.wifi.label")
|
||||
checked: Settings.data.network.wifiEnabled
|
||||
onToggled: checked => NetworkService.setWifiEnabled(checked)
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Enable Bluetooth"
|
||||
label: I18n.tr("settings.network.bluetooth.label")
|
||||
checked: Settings.data.network.bluetoothEnabled
|
||||
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
// Helper functions to update arrays immutably
|
||||
function addMonitor(list, name) {
|
||||
const arr = (list || []).slice()
|
||||
if (!arr.includes(name))
|
||||
arr.push(name)
|
||||
return arr
|
||||
}
|
||||
function removeMonitor(list, name) {
|
||||
return (list || []).filter(function (n) {
|
||||
return n !== name
|
||||
})
|
||||
}
|
||||
|
||||
// General Notification Settings
|
||||
ColumnLayout {
|
||||
spacing: Style.marginL * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NHeader {
|
||||
label: I18n.tr("settings.notifications.settings.section.label")
|
||||
description: I18n.tr("settings.notifications.settings.section.description")
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("settings.notifications.settings.do-not-disturb.label")
|
||||
description: I18n.tr("settings.notifications.settings.do-not-disturb.description")
|
||||
checked: Settings.data.notifications.doNotDisturb
|
||||
onToggled: checked => Settings.data.notifications.doNotDisturb = checked
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("settings.notifications.settings.location.label")
|
||||
description: I18n.tr("settings.notifications.settings.location.description")
|
||||
model: [{
|
||||
"key": "top",
|
||||
"name": I18n.tr("options.launcher.position.top_center")
|
||||
}, {
|
||||
"key": "top_left",
|
||||
"name": I18n.tr("options.launcher.position.top_left")
|
||||
}, {
|
||||
"key": "top_right",
|
||||
"name": I18n.tr("options.launcher.position.top_right")
|
||||
}, {
|
||||
"key": "bottom",
|
||||
"name": I18n.tr("options.launcher.position.bottom_center")
|
||||
}, {
|
||||
"key": "bottom_left",
|
||||
"name": I18n.tr("options.launcher.position.bottom_left")
|
||||
}, {
|
||||
"key": "bottom_right",
|
||||
"name": I18n.tr("options.launcher.position.bottom_right")
|
||||
}]
|
||||
currentKey: Settings.data.notifications.location || "top_right"
|
||||
onSelected: key => Settings.data.notifications.location = key
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("settings.notifications.settings.always-on-top.label")
|
||||
description: I18n.tr("settings.notifications.settings.always-on-top.description")
|
||||
checked: Settings.data.notifications.alwaysOnTop
|
||||
onToggled: checked => Settings.data.notifications.alwaysOnTop = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("settings.notifications.settings.enable-osd.label")
|
||||
description: I18n.tr("settings.notifications.settings.enable-osd.description")
|
||||
checked: Settings.data.notifications.enableOSD
|
||||
onToggled: checked => Settings.data.notifications.enableOSD = checked
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginXL * scaling
|
||||
Layout.bottomMargin: Style.marginXL * scaling
|
||||
}
|
||||
|
||||
// Duration
|
||||
ColumnLayout {
|
||||
spacing: Style.marginL * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NHeader {
|
||||
label: I18n.tr("settings.notifications.duration.section.label")
|
||||
description: I18n.tr("settings.notifications.duration.section.description")
|
||||
}
|
||||
|
||||
// Respect Expire Timeout (eg. --expire-time flag in notify-send)
|
||||
NToggle {
|
||||
label: I18n.tr("settings.notifications.duration.respect-expire.label")
|
||||
description: I18n.tr("settings.notifications.duration.respect-expire.description")
|
||||
checked: Settings.data.notifications.respectExpireTimeout
|
||||
onToggled: checked => Settings.data.notifications.respectExpireTimeout = checked
|
||||
}
|
||||
|
||||
// Low Urgency Duration
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("settings.notifications.duration.low-urgency.label")
|
||||
description: I18n.tr("settings.notifications.duration.low-urgency.description")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
from: 1
|
||||
to: 30
|
||||
stepSize: 1
|
||||
value: Settings.data.notifications.lowUrgencyDuration
|
||||
onMoved: value => Settings.data.notifications.lowUrgencyDuration = value
|
||||
text: Settings.data.notifications.lowUrgencyDuration + "s"
|
||||
}
|
||||
}
|
||||
|
||||
// Normal Urgency Duration
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("settings.notifications.duration.normal-urgency.label")
|
||||
description: I18n.tr("settings.notifications.duration.normal-urgency.description")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
from: 1
|
||||
to: 30
|
||||
stepSize: 1
|
||||
value: Settings.data.notifications.normalUrgencyDuration
|
||||
onMoved: value => Settings.data.notifications.normalUrgencyDuration = value
|
||||
text: Settings.data.notifications.normalUrgencyDuration + "s"
|
||||
}
|
||||
}
|
||||
|
||||
// Critical Urgency Duration
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("settings.notifications.duration.critical-urgency.label")
|
||||
description: I18n.tr("settings.notifications.duration.critical-urgency.description")
|
||||
}
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
from: 1
|
||||
to: 30
|
||||
stepSize: 1
|
||||
value: Settings.data.notifications.criticalUrgencyDuration
|
||||
onMoved: value => Settings.data.notifications.criticalUrgencyDuration = value
|
||||
text: Settings.data.notifications.criticalUrgencyDuration + "s"
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginXL * scaling
|
||||
Layout.bottomMargin: Style.marginXL * scaling
|
||||
}
|
||||
|
||||
// Monitor Configuration
|
||||
NHeader {
|
||||
label: I18n.tr("settings.notifications.monitors.section.label")
|
||||
description: I18n.tr("settings.notifications.monitors.section.description")
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Quickshell.screens || []
|
||||
delegate: NCheckbox {
|
||||
Layout.fillWidth: true
|
||||
label: modelData.name || I18n.tr("system.unknown")
|
||||
description: I18n.tr("system.monitor-description", {
|
||||
"model": modelData.model,
|
||||
"width": modelData.width,
|
||||
"height": modelData.height
|
||||
})
|
||||
checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors, modelData.name)
|
||||
} else {
|
||||
Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors, modelData.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginXL * scaling
|
||||
Layout.bottomMargin: Style.marginXL * scaling
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user