1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-06 13:38:28 -04:00

Compare commits

..

1 Commits

Author SHA1 Message Date
purian23 b40fbeef56 Freeze compact XBPS repository for R2 migration 2026-07-10 18:25:36 -04:00
219 changed files with 32 additions and 68721 deletions
-35
View File
@@ -1,35 +0,0 @@
---
name: Bug Report
about: Crashes or unexpected behaviors
title: ""
labels: "bug"
assignees: ""
---
## Description
<!-- Brief description of the issue -->
## Expected Behavior
<!-- Describe what you expected to happen -->
## Steps to Reproduce
<!-- Please provide detailed steps to reproduce the issue -->
1.
2.
3.
## Error Messages/Logs
<!-- Please include any error messages, stack traces, or relevant logs -->
```
Paste error messages or logs here
```
## Screenshots/Recordings
<!-- If applicable, add screenshots or screen recordings -->
-23
View File
@@ -1,23 +0,0 @@
---
name: Request a Feature
about: New widgets, new widget behavior, etc.
title: ""
labels: "enhancement"
assignees: ""
---
## Feature Description
<!-- Brief description of the feature requested -->
## Use Case
<!-- Explain the purpose of this feature/why it'd be useful to you -->
## Proposed Solution
<!-- If you have any ideas for how to implement this, please share! -->
## Alternatives/Existing Solutions
<!-- Include any similar/pre-existing products that solve this problem -->
-24
View File
@@ -1,24 +0,0 @@
---
name: Request Assistance or Support
about: Help with installation, usage, or general questions.
title: ""
labels: "support"
assignees: ""
---
## Description
<!-- Brief description of the support needed -->
## Solutions Tried
<!-- Describe what you've tried so far -->
<!-- Outlining what you've tried so far helps us make improvements to the user experience and documentation to avoid recurrent issues -->
## Configuration Details
<!-- Include any configuration if relevant -->
## Screenshots/Recordings
<!-- If applicable, add screenshots or screen recordings -->
-59
View File
@@ -1,59 +0,0 @@
name: Create Release
on:
push:
tags:
- 'v*'
concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: true
jobs:
create_release:
name: 📦 Create GitHub Release
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch full history for changelog generation
# Generate changelog
- name: Generate Changelog
id: changelog
run: |
# Get the previous tag
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
if [ -z "$PREVIOUS_TAG" ]; then
echo "No previous tag found, using all commits"
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" | head -50)
else
echo "Generating changelog from $PREVIOUS_TAG to HEAD"
CHANGELOG=$(git log --oneline --pretty=format:"- %s (%h)" $PREVIOUS_TAG..HEAD)
fi
# Create the changelog with proper formatting
cat > CHANGELOG.md << EOF
## What's Changed
$CHANGELOG
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...${{ github.ref_name }}
EOF
# Set output for use in release step
echo "changelog<<EOF" >> $GITHUB_OUTPUT
cat CHANGELOG.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Create GitHub Release
- name: Create GitHub Release
uses: comnoco/create-release-action@v2.0.5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref_name }}
release_name: Release ${{ github.ref_name }}
body: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: ${{ contains(github.ref_name, '-') }}
-99
View File
@@ -1,99 +0,0 @@
# C++ objects and libs
*.slo
*.lo
*.o
*.a
*.la
*.lai
*.so
*.so.*
*.dll
*.dylib
# Qt-es
object_script.*.Release
object_script.*.Debug
*_plugin_import.cpp
/.qmake.cache
/.qmake.stash
*.pro.user
*.pro.user.*
*.qbs.user
*.qbs.user.*
*.moc
moc_*.cpp
moc_*.h
qrc_*.cpp
ui_*.h
*.qmlc
*.jsc
Makefile*
*build-*
*.qm
*.prl
# Qt unit tests
target_wrapper.*
# QtCreator
*.autosave
# QtCreator Qml
*.qmlproject.user
*.qmlproject.user.*
# QtCreator CMake
CMakeLists.txt.user*
# QtCreator 4.8< compilation database
compile_commands.json
# QtCreator local machine specific files for imported projects
*creator.user*
*_qmlcache.qrc
UNUSED
.qmlls.ini
CLAUDE-activeContext.md
CLAUDE-temp.md
# Auto-generated theme files
*.generated.*
niri-colors.generated.kdl
ghostty-colors.generated.conf
result
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Code coverage profiles and other test artifacts
*.out
coverage.*
*.coverprofile
profile.cov
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
# Editor/IDE
# .idea/
# .vscode/
View File
-493
View File
@@ -1,493 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## AI Guidance
* Ignore GEMINI.md and GEMINI-*.md files
* After receiving tool results, carefully reflect on their quality and determine optimal next steps before proceeding. Use your thinking to plan and iterate based on this new information, and then take the best next action.
* For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.
* Before you finish, please verify your solution
* Do what has been asked; nothing more, nothing less.
* NEVER create files unless they're absolutely necessary for achieving your goal.
* ALWAYS prefer editing an existing file to creating a new one.
* NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
* When you update or modify core context files, also update markdown documentation and memory bank
* When asked to commit changes, exclude CLAUDE.md and CLAUDE-*.md referenced memory bank system files from any commits.
## Memory Bank System
This project uses a structured memory bank system with specialized context files. Always check these files for relevant information before starting work:
### Core Context Files
* **CLAUDE-activeContext.md** - Current session state, goals, and progress (if exists)
* **CLAUDE-patterns.md** - Established code patterns and conventions (if exists)
* **CLAUDE-decisions.md** - Architecture decisions and rationale (if exists)
* **CLAUDE-troubleshooting.md** - Common issues and proven solutions (if exists)
* **CLAUDE-config-variables.md** - Configuration variables reference (if exists)
* **CLAUDE-temp.md** - Temporary scratch pad (only read when referenced)
**Important:** Always reference the active context file first to understand what's currently being worked on and maintain session continuity.
### Memory Bank System Backups
When asked to backup Memory Bank System files, you will copy the core context files above and @.claude settings directory to directory @/path/to/backup-directory. If files already exist in the backup directory, you will overwrite them.
## Project Overview
## Project Overview
This is a Quickshell-based desktop shell implementation with Material Design 3 dark theme. The shell provides a complete desktop environment experience with panels, widgets, and system integration services.
**Architecture**: Modular design with clean separation between UI components (Modules), system services (Services), and shared utilities (Common).
**Compositor Support**: Originally designed for niri, now also fully compatible with Hyprland. Both compositors are supported with their own configuration examples and keybind formats.
## Technology Stack
- **QML (Qt Modeling Language)** - Primary language for all UI components
- **Quickshell Framework** - QML-based framework for building desktop shells
- **Qt/QtQuick** - UI rendering and controls
- **Wayland** - Display server protocol
- **Matugen** - Dynamic theming system for wallpaper-based colors and system app theming
## Development Commands
Since this is a Quickshell-based project without traditional build configuration files, development typically involves:
```bash
# Run the shell (requires Quickshell to be installed)
quickshell -p shell.qml
# Or use the shorthand
qs -p .
# Code formatting and linting
./qmlformat-all.sh # Format all QML files using project script
qmlformat -i **/*.qml # Format all QML files in place
qmllint **/*.qml # Lint all QML files for syntax errors
```
## Architecture Overview
### Modular Structure
The shell follows a clean modular architecture reduced from 4,830 lines to ~250 lines in shell.qml:
```
shell.qml # Main entry point (minimal orchestration)
├── Common/ # Shared resources (12 files)
│ ├── Theme.qml # Material Design 3 theme singleton
│ ├── SettingsData.qml # User preferences and configuration
│ ├── SessionData.qml # Session state management
│ ├── Colors.qml # Dynamic color scheme
│ └── [8 more utility files]
├── Services/ # System integration singletons (20 files)
│ ├── AudioService.qml
│ ├── NetworkService.qml
│ ├── BluetoothService.qml
│ ├── BrightnessService.qml
│ ├── NotificationService.qml
│ ├── WeatherService.qml
│ └── [14 more services]
├── Modules/ # UI components (93 files)
│ ├── TopBar/ # Panel components (13 files)
│ ├── ControlCenter/ # System controls (13 files)
│ ├── Notifications/ # Notification system (12 files)
│ ├── AppDrawer/ # Application launcher (3 files)
│ ├── Settings/ # Configuration interface (11 files)
│ ├── ProcessList/ # System monitoring (8 files)
│ ├── Dock/ # Application dock (6 files)
│ ├── Lock/ # Screen lock system (4 files)
│ └── [23 more module files]
├── Modals/ # Full-screen overlays (10 files)
│ ├── SettingsModal.qml
│ ├── ClipboardHistoryModal.qml
│ ├── ProcessListModal.qml
│ └── [7 more modals]
└── Widgets/ # Reusable UI controls (19 files)
├── DankIcon.qml
├── DankSlider.qml
├── DankToggle.qml
├── DankTabBar.qml
├── DankGridView.qml
├── DankListView.qml
└── [13 more widgets]
```
### Component Organization
1. **Shell Entry Point** (`shell.qml`)
- Minimal orchestration layer (~250 lines)
- Imports and instantiates components
- Handles global state and property bindings
- Multi-monitor support using Quickshell's `Variants`
2. **Common/** - Shared resources
- `Theme.qml` - Material Design 3 theme singleton with consistent colors, spacing, fonts
- `Utilities.js` - Shared functions for workspace parsing, notifications, menu handling
3. **Services/** - System integration singletons
- **Pattern**: All services use `Singleton` type with `id: root`
- **Independence**: No cross-service dependencies
- **Examples**: AudioService, NetworkService, BluetoothService, BrightnessService, WeatherService, NotificationService, CalendarService, BatteryService, NiriService, MprisController
- Services handle system commands, state management, and hardware integration
4. **Modules/** - UI components (93 files)
- **TopBar/**: Panel components with workspace switching, system indicators, media controls
- **ControlCenter/**: System controls for WiFi, Bluetooth, audio, display settings
- **Notifications/**: Complete notification system with center, popups, and keyboard navigation
- **AppDrawer/**: Application launcher with grid/list views and category filtering
- **Settings/**: Comprehensive configuration interface with multiple tabs
- **ProcessList/**: System monitoring with process management and performance metrics
- **Dock/**: Application dock with running apps and window management
- **Lock/**: Screen lock system with authentication
- **CentcomCenter/**: Calendar, weather, and media widgets
5. **Modals/** - Full-screen overlays (10 files)
- Modal system for settings, clipboard history, file browser, network info, power menu
- Unified modal management with consistent styling and keyboard navigation
6. **Widgets/** - Reusable UI controls (19 files)
- **DankIcon**: Centralized icon component with Material Design font integration
- **DankSlider**: Enhanced slider with animations and smart detection
- **DankToggle**: Consistent toggle switch component
- **DankTabBar**: Unified tab bar implementation
- **DankGridView**: Reusable grid view with adaptive columns
- **DankListView**: Reusable list view with configurable styling
- **DankTextField**: Styled text input with validation
- **DankDropdown**: Dropdown selection component
- **DankPopout**: Base popout component for overlays
- **StateLayer**: Material Design 3 interaction states
- **StyledRect/StyledText**: Themed base components
- **CachingImage**: Optimized image loading with caching
- **DankLocationSearch**: Location picker with search
- **SystemLogo**: Animated system branding component
### Key Architectural Patterns
1. **Singleton Services Pattern**:
```qml
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
property type value: defaultValue
function performAction() { /* implementation */ }
}
```
2. **Smart Feature Detection**: Services detect system capabilities:
```qml
property bool featureAvailable: false
// Auto-hide UI elements when features unavailable
visible: ServiceName.featureAvailable
```
3. **Property Bindings**: Reactive UI updates through property binding
4. **Material Design Theming**: Consistent use of Theme singleton throughout
### Important Components
- **ControlCenter**: System controls (WiFi, Bluetooth, brightness, volume, night mode)
- **AppLauncher**: Full-featured app grid/list with 93+ applications, search, categories
- **ClipboardHistoryModal**: Complete clipboard management with cliphist integration
- **TopBar**: Per-monitor panels with workspace switching, clock, system tray
- **System App Theming**: Automatic GTK and Qt application theming using matugen templates
#### Key Widgets
- **DankIcon**: Centralized icon component with automatic Material Design font detection
- **DankSlider**: Enhanced slider with animations and smart detection
- **DankToggle**: Consistent toggle switch component
- **DankTabBar**: Unified tab bar implementation
- **DankGridView**: Reusable grid view with adaptive columns
- **DankListView**: Reusable list view with configurable styling
## Code Conventions
### QML Style Guidelines
1. **Structure and Formatting**:
- Use 4-space indentation
- `id` should be the first property
- Properties before signal handlers before child components
- Prefer property bindings over imperative code
- **CRITICAL**: NEVER add comments unless absolutely essential for complex logic understanding. Code should be self-documenting through clear naming and structure. Comments are a code smell indicating unclear implementation.
2. **Naming Conventions**:
- **Services**: Use `Singleton` type with `id: root`
- **Components**: Use descriptive names (e.g., `DankSlider`, `TopBar`)
- **Properties**: camelCase for properties, PascalCase for types
3. **Null-Safe Operations**:
- **Do NOT use** `?.` operator (not supported by qmlformat)
- **Use** `object && object.property` instead of `object?.property`
- **Example**: `activePlayer && activePlayer.trackTitle` instead of `activePlayer?.trackTitle`
4. **Component Structure**:
```qml
// For regular components
Item {
id: root
property type name: value
signal customSignal(type param)
onSignal: { /* handler */ }
Component { /* children */ }
}
// For services (singletons)
Singleton {
id: root
property bool featureAvailable: false
property type currentValue: defaultValue
function performAction(param) { /* implementation */ }
}
```
### Import Guidelines
1. **Standard Import Order**:
```qml
import QtQuick
import QtQuick.Controls // If needed
import Quickshell
import Quickshell.Widgets
import Quickshell.Io // For Process, FileView
import qs.Common // For Theme, utilities
import qs.Services // For service access
import qs.Widgets // For reusable widgets (DankIcon, etc.)
```
2. **Service Dependencies**:
- Services should NOT import other services
- Modules and Widgets can import and use services via property bindings
- Use `Theme.propertyName` for consistent styling
- Use `DankIcon { name: "icon_name" }` for all icons instead of manual Text components
### Component Development Patterns
1. **Code Reuse - Search Before Writing**:
- **ALWAYS** search the codebase for existing functions before writing new ones
- Use `Grep` or `Glob` tools to find existing implementations (e.g., search for "getWifiIcon", "getDeviceIcon")
- Many utility functions already exist in Services/ and Common/ - reuse them instead of duplicating
- Examples of existing utility functions: `Theme.getBatteryIcon()`, `BluetoothService.getDeviceIcon()`, `WeatherService.getWeatherIcon()`
- If similar functionality exists, extend or refactor rather than duplicate
2. **Smart Feature Detection**:
```qml
// In services - detect capabilities
property bool brightnessAvailable: false
// In modules - adapt UI accordingly
DankSlider {
visible: BrightnessService.brightnessAvailable
enabled: BrightnessService.brightnessAvailable
value: BrightnessService.brightnessLevel
}
```
3. **Reusable Components**:
- Create reusable widgets for common patterns (like DankSlider)
- Use configurable properties for different use cases
- Include proper signal handling with unique names (avoid `valueChanged`)
4. **Service Integration**:
- Services expose properties and functions
- Modules and Widgets bind to service properties for reactive updates
- Use service functions for actions: `ServiceName.performAction(value)`
- **CRITICAL**: DO NOT create wrapper functions for everything - bind directly to underlying APIs when possible
- Example: Use `BluetoothService.adapter.discovering = true` instead of `BluetoothService.startScan()`
- Example: Use `device.connect()` directly instead of `BluetoothService.connect(device.address)`
### Error Handling and Debugging
1. **Console Logging**:
```qml
// Use appropriate log levels
console.log("Info message") // General info
console.warn("Warning message") // Warnings
console.error("Error message") // Errors
// Include context in service operations
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Service failed:", serviceName, "exit code:", exitCode)
}
}
```
2. **Graceful Degradation**:
- Always check feature availability before showing UI
- Provide fallbacks for missing system tools
- Use `visible` and `enabled` properties appropriately
## Multi-Monitor Support
The shell uses Quickshell's `Variants` pattern for multi-monitor support:
- Each connected monitor gets its own top bar instance
- Workspace switchers are compositor-aware (Niri and Hyprland)
- Monitors are automatically detected by screen name (DP-1, DP-2, etc.)
- **Niri**: Workspaces are dynamically synchronized with Niri's per-output workspaces
- **Hyprland**: Integrates with Hyprland's workspace system and multi-monitor handling
## Common Development Tasks
### Testing and Validation
When modifying the shell:
1. **Test changes**: `qs -p .` (automatic reload on file changes)
2. **Code quality**: Run `./qmlformat-all.sh` or `qmlformat -i **/*.qml` and `qmllint **/*.qml` to ensure proper formatting and syntax
3. **Performance**: Ensure animations remain smooth (60 FPS target)
4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency
5. **Wayland compatibility**: Test on Wayland session
6. **Multi-monitor**: Verify behavior with multiple displays
7. **Compositor compatibility**: Test on both Niri and Hyprland when possible
8. **Feature detection**: Test on systems with/without required tools
### Adding New Modules
1. **Create component**:
```bash
# Create new module file
touch Modules/NewModule.qml
```
2. **Follow module patterns**:
- Use `Theme.propertyName` for styling
- Import `qs.Common` and `qs.Services` as needed
- Import `qs.Widgets` for reusable components
- Bind to service properties for reactive updates
- Consider per-screen vs global behavior
- Use `DankIcon` for icons instead of manual Text components
3. **Integration in shell.qml**:
```qml
NewModule {
id: newModule
// Configure properties
}
```
### Adding New Widgets
1. **Create component**:
```bash
# Create new widget file
touch Widgets/NewWidget.qml
```
2. **Follow widget patterns**:
- Use `Theme.propertyName` for styling
- Import `qs.Common` for theming
- Focus on reusability and composition
- Keep widgets simple and focused
- Use `DankIcon` for icons instead of manual Text components
### Adding New Services
1. **Create service**:
```qml
// Services/NewService.qml
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
property bool featureAvailable: false
property type currentValue: defaultValue
function performAction(param) {
// Implementation
}
}
```
2. **Use in modules**:
```qml
// In module files
property alias serviceValue: NewService.currentValue
SomeControl {
visible: NewService.featureAvailable
enabled: NewService.featureAvailable
onTriggered: NewService.performAction(value)
}
```
### Debugging Common Issues
1. **Import errors**: Check import paths
2. **Singleton conflicts**: Ensure services use `Singleton` type with `id: root`
3. **Property binding issues**: Use property aliases for reactive updates
4. **Process failures**: Check system tool availability and command syntax
5. **Theme inconsistencies**: Always use `Theme.propertyName` instead of hardcoded values
### Best Practices Summary
- **Code Reuse**: ALWAYS search existing codebase before writing new functions - avoid duplication at all costs
- **No Comments**: Code should be self-documenting - comments indicate poor naming/structure
- **Modularity**: Keep components focused and independent
- **Reusability**: Create reusable components for common patterns using Widgets/
- **Responsiveness**: Use property bindings for reactive UI
- **Robustness**: Implement feature detection and graceful degradation
- **Consistency**: Follow Material Design 3 principles via Theme singleton
- **Performance**: Minimize expensive operations and use appropriate data structures
- **Icon Management**: Use `DankIcon` for all icons instead of manual Text components
- **Widget System**: Leverage existing widgets (DankSlider, DankToggle, etc.) for consistency
- **NO WRAPPER HELL**: Avoid creating unnecessary wrapper functions - bind directly to underlying APIs for better reactivity and performance
- **Function Discovery**: Use grep/search tools to find existing utility functions before implementing new ones
- **Modern QML Patterns**: Leverage new widgets like DankTextField, DankDropdown, CachingImage
- **Structured Organization**: Follow the established Services/Modules/Widgets/Modals separation
### Common Widget Patterns
1. **Icons**: Always use `DankIcon { name: "icon_name" }` instead of `Text { font.family: Theme.iconFont }`
2. **Sliders**: Use `DankSlider` for consistent styling and behavior
3. **Toggles**: Use `DankToggle` for switches and checkboxes
4. **Tab Bars**: Use `DankTabBar` for tabbed interfaces
5. **Lists**: Use `DankListView` for scrollable lists
6. **Grids**: Use `DankGridView` for grid layouts
7. **Text Fields**: Use `DankTextField` for text input with validation
8. **Dropdowns**: Use `DankDropdown` for selection menus
9. **Popouts**: Use `DankPopout` as base for overlay components
10. **Images**: Use `CachingImage` for optimized image loading
### Essential Utility Functions
Before writing new utility functions, check these existing ones:
**Theme.qml utilities:**
- `getBatteryIcon(level, isCharging, batteryAvailable)` - Battery status icons
- `getPowerProfileIcon(profile)` - Power profile indicators
**Service utilities:**
- `BluetoothService.getDeviceIcon(device)` - Bluetooth device type icons
- `BluetoothService.getSignalIcon(device)` - Signal strength indicators
- `WeatherService.getWeatherIcon(code)` - Weather condition icons
- `AppSearchService.getCategoryIcon(category)` - Application category icons
- `DgopService.getProcessIcon(command)` - Process type icons
- `SettingsData.getWorkspaceNameIcon(workspaceName)` - Workspace icons
**Always search for existing functions using:**
```bash
grep -r "function.*get.*Icon" Services/ Common/
grep -r "function.*" path/to/relevant/directory/
```
-25
View File
@@ -1,25 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Singleton {
id: root
readonly property int durShort: 200
readonly property int durMed: 450
readonly property int durLong: 600
readonly property int slidePx: 80
readonly property var emphasized: [0.05, 0.00, 0.133333, 0.06, 0.166667, 0.40, 0.208333, 0.82, 0.25, 1.00, 1.00, 1.00]
readonly property var emphasizedDecel: [0.05, 0.70, 0.10, 1.00, 1.00, 1.00]
readonly property var emphasizedAccel: [0.30, 0.00, 0.80, 0.15, 1.00, 1.00]
readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00]
readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00]
readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00]
}
-131
View File
@@ -1,131 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
property var appUsageRanking: {
}
Component.onCompleted: {
loadSettings()
}
function loadSettings() {
parseSettings(settingsFile.text())
}
function parseSettings(content) {
try {
if (content && content.trim()) {
var settings = JSON.parse(content)
appUsageRanking = settings.appUsageRanking || {}
}
} catch (e) {
}
}
function saveSettings() {
settingsFile.setText(JSON.stringify({
"appUsageRanking": appUsageRanking
}, null, 2))
}
function addAppUsage(app) {
if (!app)
return
var appId = app.id || (app.execString || app.exec || "")
if (!appId)
return
var currentRanking = Object.assign({}, appUsageRanking)
if (currentRanking[appId]) {
currentRanking[appId].usageCount = (currentRanking[appId].usageCount
|| 1) + 1
currentRanking[appId].lastUsed = Date.now()
currentRanking[appId].icon = app.icon || currentRanking[appId].icon
|| "application-x-executable"
currentRanking[appId].name = app.name
|| currentRanking[appId].name || ""
} else {
currentRanking[appId] = {
"name": app.name || "",
"exec": app.execString || app.exec || "",
"icon": app.icon || "application-x-executable",
"comment": app.comment || "",
"usageCount": 1,
"lastUsed": Date.now()
}
}
appUsageRanking = currentRanking
saveSettings()
}
function getAppUsageRanking() {
return appUsageRanking
}
function getRankedApps() {
var apps = []
for (var appId in appUsageRanking) {
var appData = appUsageRanking[appId]
apps.push({
"id": appId,
"name": appData.name,
"exec": appData.exec,
"icon": appData.icon,
"comment": appData.comment,
"usageCount": appData.usageCount,
"lastUsed": appData.lastUsed
})
}
return apps.sort(function (a, b) {
if (a.usageCount !== b.usageCount)
return b.usageCount - a.usageCount
return a.name.localeCompare(b.name)
})
}
function cleanupAppUsageRanking(availableAppIds) {
var currentRanking = Object.assign({}, appUsageRanking)
var hasChanges = false
for (var appId in currentRanking) {
if (availableAppIds.indexOf(appId) === -1) {
delete currentRanking[appId]
hasChanges = true
}
}
if (hasChanges) {
appUsageRanking = currentRanking
saveSettings()
}
}
FileView {
id: settingsFile
path: StandardPaths.writableLocation(
StandardPaths.GenericStateLocation) + "/DankMaterialShell/appusage.json"
blockLoading: true
blockWrites: true
watchChanges: true
onLoaded: {
parseSettings(settingsFile.text())
}
onLoadFailed: error => {}
}
}
-66
View File
@@ -1,66 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Singleton {
id: root
readonly property Rounding rounding: Rounding {}
readonly property Spacing spacing: Spacing {}
readonly property FontSize fontSize: FontSize {}
readonly property Anim anim: Anim {}
component Rounding: QtObject {
readonly property int small: 8
readonly property int normal: 12
readonly property int large: 16
readonly property int extraLarge: 24
readonly property int full: 1000
}
component Spacing: QtObject {
readonly property int small: 4
readonly property int normal: 8
readonly property int large: 12
readonly property int extraLarge: 16
readonly property int huge: 24
}
component FontSize: QtObject {
readonly property int small: 12
readonly property int normal: 14
readonly property int large: 16
readonly property int extraLarge: 20
readonly property int huge: 24
}
component AnimCurves: QtObject {
readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1]
readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
readonly property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1
/ 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
readonly property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
}
component AnimDurations: QtObject {
readonly property int quick: 150
readonly property int normal: 300
readonly property int slow: 500
readonly property int extraSlow: 1000
readonly property int expressiveFastSpatial: 350
readonly property int expressiveDefaultSpatial: 500
readonly property int expressiveEffects: 200
}
component Anim: QtObject {
readonly property AnimCurves curves: AnimCurves {}
readonly property AnimDurations durations: AnimDurations {}
}
}
-45
View File
@@ -1,45 +0,0 @@
import Quickshell
pragma Singleton
Singleton {
id: root
// Clear all image cache
function clearImageCache() {
Quickshell.execDetached(["rm", "-rf", Paths.stringify(
Paths.imagecache)])
Paths.mkdir(Paths.imagecache)
}
// Clear cache older than specified minutes
function clearOldCache(ageInMinutes) {
Quickshell.execDetached(
["find", Paths.stringify(
Paths.imagecache), "-name", "*.png", "-mmin", `+${ageInMinutes}`, "-delete"])
}
// Clear cache for specific size
function clearCacheForSize(size) {
Quickshell.execDetached(
["find", Paths.stringify(
Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"])
}
// Get cache size in MB
function getCacheSize(callback) {
var process = Qt.createQmlObject(`
import Quickshell.Io
Process {
command: ["du", "-sm", "${Paths.stringify(
Paths.imagecache)}"]
running: true
stdout: StdioCollector {
onStreamFinished: {
var sizeMB = parseInt(text.split("\\t")[0]) || 0
callback(sizeMB)
}
}
}
`, root)
}
}
-14
View File
@@ -1,14 +0,0 @@
import QtQuick
pragma Singleton
QtObject {
id: modalManager
signal closeAllModalsExcept(var excludedModal)
function openModal(modal) {
if (!modal.allowStacking) {
closeAllModalsExcept(modal)
}
}
}
-48
View File
@@ -1,48 +0,0 @@
pragma Singleton
import Quickshell
import QtCore
Singleton {
id: root
readonly property url home: StandardPaths.standardLocations(
StandardPaths.HomeLocation)[0]
readonly property url pictures: StandardPaths.standardLocations(
StandardPaths.PicturesLocation)[0]
readonly property url data: `${StandardPaths.standardLocations(
StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
readonly property url state: `${StandardPaths.standardLocations(
StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
readonly property url cache: `${StandardPaths.standardLocations(
StandardPaths.GenericCacheLocation)[0]}/DankMaterialShell`
readonly property url config: `${StandardPaths.standardLocations(
StandardPaths.GenericConfigLocation)[0]}/DankMaterialShell`
readonly property url imagecache: `${cache}/imagecache`
function stringify(path: url): string {
return path.toString().replace(/%20/g, " ")
}
function expandTilde(path: string): string {
return strip(path.replace("~", stringify(root.home)))
}
function shortenHome(path: string): string {
return path.replace(strip(root.home), "~")
}
function strip(path: url): string {
return stringify(path).replace("file://", "")
}
function mkdir(path: url): void {
Quickshell.execDetached(["mkdir", "-p", strip(path)])
}
function copy(from: url, to: url): void {
Quickshell.execDetached(["cp", strip(from), strip(to)])
}
}
-9
View File
@@ -1,9 +0,0 @@
import QtQuick
import Quickshell
QtObject {
required property Singleton service
Component.onCompleted: service.refCount++
Component.onDestruction: service.refCount--
}
-310
View File
@@ -1,310 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Services
Singleton {
id: root
property bool isLightMode: false
property string wallpaperPath: ""
property string wallpaperLastPath: ""
property string profileLastPath: ""
property bool doNotDisturb: false
property bool nightModeEnabled: false
property int nightModeTemperature: 4500
property var pinnedApps: []
property int selectedGpuIndex: 0
property bool nvidiaGpuTempEnabled: false
property bool nonNvidiaGpuTempEnabled: false
property var enabledGpuPciIds: []
property bool wallpaperCyclingEnabled: false
property string wallpaperCyclingMode: "interval" // "interval" or "time"
property int wallpaperCyclingInterval: 300 // seconds (5 minutes)
property string wallpaperCyclingTime: "06:00" // HH:mm format
property string lastBrightnessDevice: ""
Component.onCompleted: {
loadSettings()
}
function loadSettings() {
parseSettings(settingsFile.text())
}
function parseSettings(content) {
try {
if (content && content.trim()) {
var settings = JSON.parse(content)
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false
wallpaperPath = settings.wallpaperPath !== undefined ? settings.wallpaperPath : ""
wallpaperLastPath = settings.wallpaperLastPath
!== undefined ? settings.wallpaperLastPath : ""
profileLastPath = settings.profileLastPath
!== undefined ? settings.profileLastPath : ""
doNotDisturb = settings.doNotDisturb !== undefined ? settings.doNotDisturb : false
nightModeEnabled = settings.nightModeEnabled
!== undefined ? settings.nightModeEnabled : false
nightModeTemperature = settings.nightModeTemperature
!== undefined ? settings.nightModeTemperature : 4500
pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : []
selectedGpuIndex = settings.selectedGpuIndex
!== undefined ? settings.selectedGpuIndex : 0
nvidiaGpuTempEnabled = settings.nvidiaGpuTempEnabled
!== undefined ? settings.nvidiaGpuTempEnabled : false
nonNvidiaGpuTempEnabled = settings.nonNvidiaGpuTempEnabled
!== undefined ? settings.nonNvidiaGpuTempEnabled : false
enabledGpuPciIds = settings.enabledGpuPciIds
!== undefined ? settings.enabledGpuPciIds : []
wallpaperCyclingEnabled = settings.wallpaperCyclingEnabled
!== undefined ? settings.wallpaperCyclingEnabled : false
wallpaperCyclingMode = settings.wallpaperCyclingMode
!== undefined ? settings.wallpaperCyclingMode : "interval"
wallpaperCyclingInterval = settings.wallpaperCyclingInterval
!== undefined ? settings.wallpaperCyclingInterval : 300
wallpaperCyclingTime = settings.wallpaperCyclingTime
!== undefined ? settings.wallpaperCyclingTime : "06:00"
lastBrightnessDevice = settings.lastBrightnessDevice
!== undefined ? settings.lastBrightnessDevice : ""
}
} catch (e) {
}
}
function saveSettings() {
settingsFile.setText(JSON.stringify({
"isLightMode": isLightMode,
"wallpaperPath": wallpaperPath,
"wallpaperLastPath": wallpaperLastPath,
"profileLastPath": profileLastPath,
"doNotDisturb": doNotDisturb,
"nightModeEnabled": nightModeEnabled,
"nightModeTemperature": nightModeTemperature,
"pinnedApps": pinnedApps,
"selectedGpuIndex": selectedGpuIndex,
"nvidiaGpuTempEnabled": nvidiaGpuTempEnabled,
"nonNvidiaGpuTempEnabled": nonNvidiaGpuTempEnabled,
"enabledGpuPciIds": enabledGpuPciIds,
"wallpaperCyclingEnabled": wallpaperCyclingEnabled,
"wallpaperCyclingMode": wallpaperCyclingMode,
"wallpaperCyclingInterval": wallpaperCyclingInterval,
"wallpaperCyclingTime": wallpaperCyclingTime,
"lastBrightnessDevice": lastBrightnessDevice
}, null, 2))
}
function setLightMode(lightMode) {
isLightMode = lightMode
saveSettings()
}
function setDoNotDisturb(enabled) {
doNotDisturb = enabled
saveSettings()
}
function setNightModeEnabled(enabled) {
nightModeEnabled = enabled
saveSettings()
}
function setNightModeTemperature(temperature) {
nightModeTemperature = temperature
saveSettings()
}
function setWallpaperPath(path) {
wallpaperPath = path
saveSettings()
}
function setWallpaper(imagePath) {
wallpaperPath = imagePath
saveSettings()
if (typeof Theme !== "undefined" && typeof SettingsData !== "undefined"
&& SettingsData.wallpaperDynamicTheming) {
Theme.extractColors()
}
}
function setWallpaperLastPath(path) {
wallpaperLastPath = path
saveSettings()
}
function setProfileLastPath(path) {
profileLastPath = path
saveSettings()
}
function setPinnedApps(apps) {
pinnedApps = apps
saveSettings()
}
function addPinnedApp(appId) {
if (!appId)
return
var currentPinned = [...pinnedApps]
if (currentPinned.indexOf(appId) === -1) {
currentPinned.push(appId)
setPinnedApps(currentPinned)
}
}
function removePinnedApp(appId) {
if (!appId)
return
var currentPinned = pinnedApps.filter(id => id !== appId)
setPinnedApps(currentPinned)
}
function isPinnedApp(appId) {
return appId && pinnedApps.indexOf(appId) !== -1
}
function setSelectedGpuIndex(index) {
selectedGpuIndex = index
saveSettings()
}
function setNvidiaGpuTempEnabled(enabled) {
nvidiaGpuTempEnabled = enabled
saveSettings()
}
function setNonNvidiaGpuTempEnabled(enabled) {
nonNvidiaGpuTempEnabled = enabled
saveSettings()
}
function setEnabledGpuPciIds(pciIds) {
enabledGpuPciIds = pciIds
saveSettings()
}
function setWallpaperCyclingEnabled(enabled) {
wallpaperCyclingEnabled = enabled
saveSettings()
}
function setWallpaperCyclingMode(mode) {
wallpaperCyclingMode = mode
saveSettings()
}
function setWallpaperCyclingInterval(interval) {
wallpaperCyclingInterval = interval
saveSettings()
}
function setWallpaperCyclingTime(time) {
wallpaperCyclingTime = time
saveSettings()
}
function setLastBrightnessDevice(device) {
lastBrightnessDevice = device
saveSettings()
}
FileView {
id: settingsFile
path: StandardPaths.writableLocation(
StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
blockLoading: true
blockWrites: true
watchChanges: true
onLoaded: {
parseSettings(settingsFile.text())
}
onLoadFailed: error => {}
}
IpcHandler {
target: "wallpaper"
function get(): string {
return root.wallpaperPath || ""
}
function set(path: string): string {
if (!path) {
return "ERROR: No path provided"
}
var absolutePath = path.startsWith(
"/") ? path : StandardPaths.writableLocation(
StandardPaths.HomeLocation) + "/" + path
try {
root.setWallpaper(absolutePath)
return "SUCCESS: Wallpaper set to " + absolutePath
} catch (e) {
return "ERROR: Failed to set wallpaper: " + e.toString()
}
}
function clear(): string {
root.setWallpaper("")
return "SUCCESS: Wallpaper cleared"
}
function next(): string {
if (!root.wallpaperPath) {
return "ERROR: No wallpaper set"
}
try {
WallpaperCyclingService.cycleNextManually()
return "SUCCESS: Cycling to next wallpaper"
} catch (e) {
return "ERROR: Failed to cycle wallpaper: " + e.toString()
}
}
function prev(): string {
if (!root.wallpaperPath) {
return "ERROR: No wallpaper set"
}
try {
WallpaperCyclingService.cyclePrevManually()
return "SUCCESS: Cycling to previous wallpaper"
} catch (e) {
return "ERROR: Failed to cycle wallpaper: " + e.toString()
}
}
}
IpcHandler {
target: "theme"
function toggle(): string {
root.setLightMode(!root.isLightMode)
return root.isLightMode ? "light" : "dark"
}
function light(): string {
root.setLightMode(true)
return "light"
}
function dark(): string {
root.setLightMode(false)
return "dark"
}
function getMode(): string {
return root.isLightMode ? "light" : "dark"
}
}
}
File diff suppressed because it is too large Load Diff
-380
View File
@@ -1,380 +0,0 @@
// Stock theme definitions for DankMaterialShell
// Separated from Theme.qml to keep that file clean
const StockThemes = {
DARK: {
blue: {
name: "Blue",
primary: "#42a5f5",
primaryText: "#ffffff",
primaryContainer: "#1976d2",
secondary: "#8ab4f8",
surface: "#1a1c1e",
surfaceText: "#e3e8ef",
surfaceVariant: "#44464f",
surfaceVariantText: "#c4c7c5",
surfaceTint: "#8ab4f8",
background: "#1a1c1e",
backgroundText: "#e3e8ef",
outline: "#8e918f",
surfaceContainer: "#1e2023",
surfaceContainerHigh: "#292b2f"
},
deepBlue: {
name: "Deep Blue",
primary: "#0061a4",
primaryText: "#ffffff",
primaryContainer: "#004881",
secondary: "#42a5f5",
surface: "#1a1c1e",
surfaceText: "#e3e8ef",
surfaceVariant: "#44464f",
surfaceVariantText: "#c4c7c5",
surfaceTint: "#8ab4f8",
background: "#1a1c1e",
backgroundText: "#e3e8ef",
outline: "#8e918f",
surfaceContainer: "#1e2023",
surfaceContainerHigh: "#292b2f"
},
purple: {
name: "Purple",
primary: "#D0BCFF",
primaryText: "#381E72",
primaryContainer: "#4F378B",
secondary: "#CCC2DC",
surface: "#10121E",
surfaceText: "#E6E0E9",
surfaceVariant: "#49454F",
surfaceVariantText: "#CAC4D0",
surfaceTint: "#D0BCFF",
background: "#10121E",
backgroundText: "#E6E0E9",
outline: "#938F99",
surfaceContainer: "#1D1B20",
surfaceContainerHigh: "#2B2930"
},
green: {
name: "Green",
primary: "#4caf50",
primaryText: "#ffffff",
primaryContainer: "#388e3c",
secondary: "#81c995",
surface: "#0f1411",
surfaceText: "#e1f5e3",
surfaceVariant: "#404943",
surfaceVariantText: "#c1cbc4",
surfaceTint: "#81c995",
background: "#0f1411",
backgroundText: "#e1f5e3",
outline: "#8b938c",
surfaceContainer: "#1a1f1b",
surfaceContainerHigh: "#252a26"
},
orange: {
name: "Orange",
primary: "#ff6d00",
primaryText: "#ffffff",
primaryContainer: "#e65100",
secondary: "#ffb74d",
surface: "#1c1410",
surfaceText: "#f5f1ea",
surfaceVariant: "#4a453a",
surfaceVariantText: "#cbc5b8",
surfaceTint: "#ffb74d",
background: "#1c1410",
backgroundText: "#f5f1ea",
outline: "#958f84",
surfaceContainer: "#211e17",
surfaceContainerHigh: "#2c291f"
},
red: {
name: "Red",
primary: "#f44336",
primaryText: "#ffffff",
primaryContainer: "#d32f2f",
secondary: "#f28b82",
surface: "#1c1011",
surfaceText: "#f5e8ea",
surfaceVariant: "#4a3f41",
surfaceVariantText: "#cbc2c4",
surfaceTint: "#f28b82",
background: "#1c1011",
backgroundText: "#f5e8ea",
outline: "#958b8d",
surfaceContainer: "#211b1c",
surfaceContainerHigh: "#2c2426"
},
cyan: {
name: "Cyan",
primary: "#00bcd4",
primaryText: "#ffffff",
primaryContainer: "#0097a7",
secondary: "#4dd0e1",
surface: "#0f1617",
surfaceText: "#e8f4f5",
surfaceVariant: "#3f474a",
surfaceVariantText: "#c2c9cb",
surfaceTint: "#4dd0e1",
background: "#0f1617",
backgroundText: "#e8f4f5",
outline: "#8c9194",
surfaceContainer: "#1a1f20",
surfaceContainerHigh: "#252b2c"
},
pink: {
name: "Pink",
primary: "#e91e63",
primaryText: "#ffffff",
primaryContainer: "#c2185b",
secondary: "#f8bbd9",
surface: "#1a1014",
surfaceText: "#f3e8ee",
surfaceVariant: "#483f45",
surfaceVariantText: "#c9c2c7",
surfaceTint: "#f8bbd9",
background: "#1a1014",
backgroundText: "#f3e8ee",
outline: "#938a90",
surfaceContainer: "#1f1b1e",
surfaceContainerHigh: "#2a2428"
},
amber: {
name: "Amber",
primary: "#ffc107",
primaryText: "#000000",
primaryContainer: "#ff8f00",
secondary: "#ffd54f",
surface: "#1a1710",
surfaceText: "#f3f0e8",
surfaceVariant: "#49453a",
surfaceVariantText: "#cac5b8",
surfaceTint: "#ffd54f",
background: "#1a1710",
backgroundText: "#f3f0e8",
outline: "#949084",
surfaceContainer: "#1f1e17",
surfaceContainerHigh: "#2a281f"
},
coral: {
name: "Coral",
primary: "#ffb4ab",
primaryText: "#5f1412",
primaryContainer: "#8c1d18",
secondary: "#f9dedc",
surface: "#1a1110",
surfaceText: "#f1e8e7",
surfaceVariant: "#4a4142",
surfaceVariantText: "#cdc2c1",
surfaceTint: "#ffb4ab",
background: "#1a1110",
backgroundText: "#f1e8e7",
outline: "#968b8a",
surfaceContainer: "#201a19",
surfaceContainerHigh: "#2b2221"
}
},
LIGHT: {
blue: {
name: "Blue Light",
primary: "#1976d2",
primaryText: "#ffffff",
primaryContainer: "#e3f2fd",
secondary: "#42a5f5",
surface: "#fefefe",
surfaceText: "#1a1c1e",
surfaceVariant: "#e7e0ec",
surfaceVariantText: "#49454f",
surfaceTint: "#1976d2",
background: "#fefefe",
backgroundText: "#1a1c1e",
outline: "#79747e",
surfaceContainer: "#f3f3f3",
surfaceContainerHigh: "#ececec"
},
deepBlue: {
name: "Deep Blue Light",
primary: "#0061a4",
primaryText: "#ffffff",
primaryContainer: "#cfe5ff",
secondary: "#1976d2",
surface: "#fefefe",
surfaceText: "#1a1c1e",
surfaceVariant: "#e7e0ec",
surfaceVariantText: "#49454f",
surfaceTint: "#0061a4",
background: "#fefefe",
backgroundText: "#1a1c1e",
outline: "#79747e",
surfaceContainer: "#f3f3f3",
surfaceContainerHigh: "#ececec"
},
purple: {
name: "Purple Light",
primary: "#6750A4",
primaryText: "#ffffff",
primaryContainer: "#EADDFF",
secondary: "#625B71",
surface: "#FFFBFE",
surfaceText: "#1C1B1F",
surfaceVariant: "#E7E0EC",
surfaceVariantText: "#49454F",
surfaceTint: "#6750A4",
background: "#FFFBFE",
backgroundText: "#1C1B1F",
outline: "#79747E",
surfaceContainer: "#F3EDF7",
surfaceContainerHigh: "#ECE6F0"
},
green: {
name: "Green Light",
primary: "#2e7d32",
primaryText: "#ffffff",
primaryContainer: "#e8f5e8",
secondary: "#4caf50",
surface: "#fefefe",
surfaceText: "#1a1c1e",
surfaceVariant: "#e7e0ec",
surfaceVariantText: "#49454f",
surfaceTint: "#2e7d32",
background: "#fefefe",
backgroundText: "#1a1c1e",
outline: "#79747e",
surfaceContainer: "#f3f3f3",
surfaceContainerHigh: "#ececec"
},
orange: {
name: "Orange Light",
primary: "#e65100",
primaryText: "#ffffff",
primaryContainer: "#ffecb3",
secondary: "#ff9800",
surface: "#fefefe",
surfaceText: "#1a1c1e",
surfaceVariant: "#e7e0ec",
surfaceVariantText: "#49454f",
surfaceTint: "#e65100",
background: "#fefefe",
backgroundText: "#1a1c1e",
outline: "#79747e",
surfaceContainer: "#f3f3f3",
surfaceContainerHigh: "#ececec"
},
red: {
name: "Red Light",
primary: "#d32f2f",
primaryText: "#ffffff",
primaryContainer: "#ffebee",
secondary: "#f44336",
surface: "#fefefe",
surfaceText: "#1a1c1e",
surfaceVariant: "#e7e0ec",
surfaceVariantText: "#49454f",
surfaceTint: "#d32f2f",
background: "#fefefe",
backgroundText: "#1a1c1e",
outline: "#79747e",
surfaceContainer: "#f3f3f3",
surfaceContainerHigh: "#ececec"
},
cyan: {
name: "Cyan Light",
primary: "#0097a7",
primaryText: "#ffffff",
primaryContainer: "#e0f2f1",
secondary: "#00bcd4",
surface: "#fefefe",
surfaceText: "#1a1c1e",
surfaceVariant: "#e7e0ec",
surfaceVariantText: "#49454f",
surfaceTint: "#0097a7",
background: "#fefefe",
backgroundText: "#1a1c1e",
outline: "#79747e",
surfaceContainer: "#f3f3f3",
surfaceContainerHigh: "#ececec"
},
pink: {
name: "Pink Light",
primary: "#c2185b",
primaryText: "#ffffff",
primaryContainer: "#fce4ec",
secondary: "#e91e63",
surface: "#fefefe",
surfaceText: "#1a1c1e",
surfaceVariant: "#e7e0ec",
surfaceVariantText: "#49454f",
surfaceTint: "#c2185b",
background: "#fefefe",
backgroundText: "#1a1c1e",
outline: "#79747e",
surfaceContainer: "#f3f3f3",
surfaceContainerHigh: "#ececec"
},
amber: {
name: "Amber Light",
primary: "#ff8f00",
primaryText: "#000000",
primaryContainer: "#fff8e1",
secondary: "#ffc107",
surface: "#fefefe",
surfaceText: "#1a1c1e",
surfaceVariant: "#e7e0ec",
surfaceVariantText: "#49454f",
surfaceTint: "#ff8f00",
background: "#fefefe",
backgroundText: "#1a1c1e",
outline: "#79747e",
surfaceContainer: "#f3f3f3",
surfaceContainerHigh: "#ececec"
},
coral: {
name: "Coral Light",
primary: "#8c1d18",
primaryText: "#ffffff",
primaryContainer: "#ffdad6",
secondary: "#ff5449",
surface: "#fefefe",
surfaceText: "#1a1c1e",
surfaceVariant: "#e7e0ec",
surfaceVariantText: "#49454f",
surfaceTint: "#8c1d18",
background: "#fefefe",
backgroundText: "#1a1c1e",
outline: "#79747e",
surfaceContainer: "#f3f3f3",
surfaceContainerHigh: "#ececec"
}
}
}
const ThemeNames = {
BLUE: "blue",
DEEP_BLUE: "deepBlue",
PURPLE: "purple",
GREEN: "green",
ORANGE: "orange",
RED: "red",
CYAN: "cyan",
PINK: "pink",
AMBER: "amber",
CORAL: "coral",
DYNAMIC: "dynamic"
}
function isStockTheme(themeName) {
return Object.keys(StockThemes.DARK).includes(themeName)
}
function getAvailableThemes(isLight = false) {
return isLight ? StockThemes.LIGHT : StockThemes.DARK
}
function getThemeByName(themeName, isLight = false) {
const themes = getAvailableThemes(isLight)
return themes[themeName] || themes.blue
}
function getAllThemeNames() {
return Object.keys(StockThemes.DARK)
}
-676
View File
@@ -1,676 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.UPower
import qs.Services
import "StockThemes.js" as StockThemes
Singleton {
id: root
property string currentTheme: "blue"
property bool isLightMode: false
readonly property string dynamic: "dynamic"
readonly property string homeDir: {
const url = StandardPaths.writableLocation(StandardPaths.HomeLocation).toString()
return url.startsWith("file://") ? url.substring(7) : url
}
readonly property string configDir: {
const url = StandardPaths.writableLocation(StandardPaths.ConfigLocation).toString()
return url.startsWith("file://") ? url.substring(7) : url
}
readonly property string shellDir: Qt.resolvedUrl(".").toString().replace("file://", "").replace("/Common/", "")
readonly property string wallpaperPath: typeof SessionData !== "undefined" ? SessionData.wallpaperPath : ""
property bool matugenAvailable: false
property bool gtkThemingEnabled: typeof SettingsData !== "undefined" ? SettingsData.gtkAvailable : false
property bool qtThemingEnabled: typeof SettingsData !== "undefined" ? (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) : false
property bool systemThemeGenerationInProgress: false
property var matugenColors: ({})
property bool extractionRequested: false
property int colorUpdateTrigger: 0
property var customThemeData: null
function getMatugenColor(path, fallback) {
colorUpdateTrigger
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode]
for (const part of path.split(".")) {
if (!cur || typeof cur !== "object" || !(part in cur))
return fallback
cur = cur[part]
}
return cur || fallback
}
readonly property var currentThemeData: {
if (currentTheme === "custom") {
return customThemeData || StockThemes.getThemeByName("blue", isLightMode)
} else if (currentTheme === dynamic) {
return {
primary: getMatugenColor("primary", "#42a5f5"),
primaryText: getMatugenColor("on_primary", "#ffffff"),
primaryContainer: getMatugenColor("primary_container", "#1976d2"),
secondary: getMatugenColor("secondary", "#8ab4f8"),
surface: getMatugenColor("surface", "#1a1c1e"),
surfaceText: getMatugenColor("on_background", "#e3e8ef"),
surfaceVariant: getMatugenColor("surface_variant", "#44464f"),
surfaceVariantText: getMatugenColor("on_surface_variant", "#c4c7c5"),
surfaceTint: getMatugenColor("surface_tint", "#8ab4f8"),
background: getMatugenColor("background", "#1a1c1e"),
backgroundText: getMatugenColor("on_background", "#e3e8ef"),
outline: getMatugenColor("outline", "#8e918f"),
surfaceContainer: getMatugenColor("surface_container", "#1e2023"),
surfaceContainerHigh: getMatugenColor("surface_container_high", "#292b2f"),
error: "#F2B8B5",
warning: "#FF9800",
info: "#2196F3",
success: "#4CAF50"
}
} else {
return StockThemes.getThemeByName(currentTheme, isLightMode)
}
}
property color primary: currentThemeData.primary
property color primaryText: currentThemeData.primaryText
property color primaryContainer: currentThemeData.primaryContainer
property color secondary: currentThemeData.secondary
property color surface: currentThemeData.surface
property color surfaceText: currentThemeData.surfaceText
property color surfaceVariant: currentThemeData.surfaceVariant
property color surfaceVariantText: currentThemeData.surfaceVariantText
property color surfaceTint: currentThemeData.surfaceTint
property color background: currentThemeData.background
property color backgroundText: currentThemeData.backgroundText
property color outline: currentThemeData.outline
property color surfaceContainer: currentThemeData.surfaceContainer
property color surfaceContainerHigh: currentThemeData.surfaceContainerHigh
property color error: currentThemeData.error || "#F2B8B5"
property color warning: currentThemeData.warning || "#FF9800"
property color info: currentThemeData.info || "#2196F3"
property color tempWarning: "#ff9933"
property color tempDanger: "#ff5555"
property color success: currentThemeData.success || "#4CAF50"
property color primaryHover: Qt.rgba(primary.r, primary.g, primary.b, 0.12)
property color primaryHoverLight: Qt.rgba(primary.r, primary.g, primary.b, 0.08)
property color primaryPressed: Qt.rgba(primary.r, primary.g, primary.b, 0.16)
property color primarySelected: Qt.rgba(primary.r, primary.g, primary.b, 0.3)
property color primaryBackground: Qt.rgba(primary.r, primary.g, primary.b, 0.04)
property color secondaryHover: Qt.rgba(secondary.r, secondary.g, secondary.b, 0.08)
property color surfaceHover: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.08)
property color surfacePressed: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.12)
property color surfaceSelected: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.15)
property color surfaceLight: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.1)
property color surfaceVariantAlpha: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.2)
property color surfaceTextHover: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.08)
property color surfaceTextAlpha: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.3)
property color surfaceTextLight: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.06)
property color surfaceTextMedium: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.7)
property color outlineButton: Qt.rgba(outline.r, outline.g, outline.b, 0.5)
property color outlineLight: Qt.rgba(outline.r, outline.g, outline.b, 0.05)
property color outlineMedium: Qt.rgba(outline.r, outline.g, outline.b, 0.08)
property color outlineStrong: Qt.rgba(outline.r, outline.g, outline.b, 0.12)
property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12)
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
property int shortDuration: 150
property int mediumDuration: 300
property int longDuration: 500
property int extraLongDuration: 1000
property int standardEasing: Easing.OutCubic
property int emphasizedEasing: Easing.OutQuart
property real cornerRadius: typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12
property real spacingXS: 4
property real spacingS: 8
property real spacingM: 12
property real spacingL: 16
property real spacingXL: 24
property real fontSizeSmall: 12
property real fontSizeMedium: 14
property real fontSizeLarge: 16
property real fontSizeXLarge: 20
property real barHeight: 48
property real iconSize: 24
property real iconSizeSmall: 16
property real iconSizeLarge: 32
property real panelTransparency: 0.85
property real widgetTransparency: typeof SettingsData !== "undefined" && SettingsData.topBarWidgetTransparency !== undefined ? SettingsData.topBarWidgetTransparency : 0.85
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 0.92
function switchTheme(themeName, savePrefs = true) {
if (themeName === dynamic) {
currentTheme = dynamic
extractColors()
} else if (themeName === "custom") {
currentTheme = "custom"
if (typeof SettingsData !== "undefined" && SettingsData.customThemeFile) {
loadCustomThemeFromFile(SettingsData.customThemeFile)
}
} else {
currentTheme = themeName
}
if (savePrefs && typeof SettingsData !== "undefined")
SettingsData.setTheme(currentTheme)
generateSystemThemesFromCurrentTheme()
}
function toggleLightMode(savePrefs = true) {
isLightMode = !isLightMode
if (savePrefs && typeof SessionData !== "undefined")
SessionData.setLightMode(isLightMode)
generateSystemThemesFromCurrentTheme()
}
function forceGenerateSystemThemes() {
if (!matugenAvailable) {
if (typeof ToastService !== "undefined") {
ToastService.showWarning("matugen not available - cannot generate system themes")
}
return
}
generateSystemThemesFromCurrentTheme()
}
function getAvailableThemes() {
return StockThemes.getAllThemeNames()
}
function getThemeDisplayName(themeName) {
const themeData = StockThemes.getThemeByName(themeName, isLightMode)
return themeData.name
}
function getThemeColors(themeName) {
if (themeName === "custom" && customThemeData) {
return customThemeData
}
return StockThemes.getThemeByName(themeName, isLightMode)
}
function loadCustomTheme(themeData) {
if (themeData.dark || themeData.light) {
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
const selectedTheme = themeData[colorMode] || themeData.dark || themeData.light
customThemeData = selectedTheme
} else {
customThemeData = themeData
}
generateSystemThemesFromCurrentTheme()
}
function loadCustomThemeFromFile(filePath) {
customThemeFileView.path = filePath
}
property alias availableThemeNames: root._availableThemeNames
readonly property var _availableThemeNames: StockThemes.getAllThemeNames()
property string currentThemeName: currentTheme
function popupBackground() {
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency)
}
function contentBackground() {
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, popupTransparency)
}
function panelBackground() {
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, panelTransparency)
}
function widgetBackground() {
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, widgetTransparency)
}
function getPopupBackgroundAlpha() {
return popupTransparency
}
function getContentBackgroundAlpha() {
return popupTransparency
}
function isColorDark(c) {
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5
}
function getBatteryIcon(level, isCharging, batteryAvailable) {
if (!batteryAvailable)
return _getBatteryPowerProfileIcon()
if (isCharging) {
if (level >= 90) return "battery_charging_full"
if (level >= 80) return "battery_charging_90"
if (level >= 60) return "battery_charging_80"
if (level >= 50) return "battery_charging_60"
if (level >= 30) return "battery_charging_50"
if (level >= 20) return "battery_charging_30"
return "battery_charging_20"
} else {
if (level >= 95) return "battery_full"
if (level >= 85) return "battery_6_bar"
if (level >= 70) return "battery_5_bar"
if (level >= 55) return "battery_4_bar"
if (level >= 40) return "battery_3_bar"
if (level >= 25) return "battery_2_bar"
if (level >= 10) return "battery_1_bar"
return "battery_alert"
}
}
function _getBatteryPowerProfileIcon() {
if (typeof PowerProfiles === "undefined")
return "balance"
switch (PowerProfiles.profile) {
case PowerProfile.PowerSaver:
return "energy_savings_leaf"
case PowerProfile.Performance:
return "rocket_launch"
default:
return "balance"
}
}
function getPowerProfileIcon(profile) {
switch (profile) {
case PowerProfile.PowerSaver:
return "battery_saver"
case PowerProfile.Balanced:
return "battery_std"
case PowerProfile.Performance:
return "flash_on"
default:
return "settings"
}
}
function getPowerProfileLabel(profile) {
switch (profile) {
case PowerProfile.PowerSaver:
return "Power Saver"
case PowerProfile.Balanced:
return "Balanced"
case PowerProfile.Performance:
return "Performance"
default:
return profile.charAt(0).toUpperCase() + profile.slice(1)
}
}
function getPowerProfileDescription(profile) {
switch (profile) {
case PowerProfile.PowerSaver:
return "Extend battery life"
case PowerProfile.Balanced:
return "Balance power and performance"
case PowerProfile.Performance:
return "Prioritize performance"
default:
return "Custom power profile"
}
}
function extractColors() {
extractionRequested = true
if (matugenAvailable)
fileChecker.running = true
else
matugenCheck.running = true
}
function onLightModeChanged() {
if (matugenColors && Object.keys(matugenColors).length > 0) {
colorUpdateTrigger++
}
if (currentTheme === "custom" && customThemeFileView.path) {
customThemeFileView.reload()
}
generateSystemThemesFromCurrentTheme()
}
function generateSystemThemes() {
if (systemThemeGenerationInProgress || !matugenAvailable || !wallpaperPath)
return
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false"
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
systemThemeGenerationInProgress = true
systemThemeGenerator.command = [shellDir + "/scripts/matugen.sh", wallpaperPath, shellDir, configDir, "generate", isLight, iconTheme]
systemThemeGenerator.running = true
}
function generateSystemThemesFromCurrentTheme() {
if (systemThemeGenerationInProgress || !matugenAvailable)
return
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false"
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
if (currentTheme === dynamic) {
if (!wallpaperPath)
return
systemThemeGenerationInProgress = true
systemThemeGenerator.command = [shellDir + "/scripts/matugen.sh", wallpaperPath, shellDir, configDir, "generate", isLight, iconTheme]
systemThemeGenerator.running = true
} else {
let primaryColor
if (currentTheme === "custom") {
if (!customThemeData || !customThemeData.primary) {
console.warn("Custom theme data not available for system theme generation")
return
}
primaryColor = customThemeData.primary
} else {
primaryColor = currentThemeData.primary
}
if (!primaryColor)
return
systemThemeGenerationInProgress = true
systemThemeGenerator.command = [shellDir + "/scripts/matugen.sh", primaryColor, shellDir, configDir, "generate-color", isLight, iconTheme]
systemThemeGenerator.running = true
}
}
function applyGtkColors() {
if (!matugenAvailable) {
if (typeof ToastService !== "undefined") {
ToastService.showError("matugen not available - cannot apply GTK colors")
}
return
}
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false"
gtkApplier.command = [shellDir + "/scripts/gtk.sh", configDir, isLight, shellDir]
gtkApplier.running = true
}
function applyQtColors() {
if (!matugenAvailable) {
if (typeof ToastService !== "undefined") {
ToastService.showError("matugen not available - cannot apply Qt colors")
}
return
}
qtApplier.command = [shellDir + "/scripts/qt.sh", configDir]
qtApplier.running = true
}
function extractJsonFromText(text) {
if (!text) return null
const start = text.search(/[{\[]/)
if (start === -1) return null
const open = text[start]
const pairs = { "{": '}', "[": ']' }
const close = pairs[open]
if (!close) return null
let inString = false
let escape = false
const stack = [open]
for (var i = start + 1; i < text.length; i++) {
const ch = text[i]
if (inString) {
if (escape) {
escape = false
} else if (ch === '\\') {
escape = true
} else if (ch === '"') {
inString = false
}
continue
}
if (ch === '"') {
inString = true
continue
}
if (ch === '{' || ch === '[') {
stack.push(ch)
continue
}
if (ch === '}' || ch === ']') {
const last = stack.pop()
if (!last || pairs[last] !== ch) {
return null
}
if (stack.length === 0) {
return text.slice(start, i + 1)
}
}
}
return null
}
Process {
id: matugenCheck
command: ["which", "matugen"]
onExited: code => {
matugenAvailable = (code === 0)
if (!matugenAvailable) {
if (typeof ToastService !== "undefined") {
ToastService.wallpaperErrorStatus = "matugen_missing"
ToastService.showWarning("matugen not found - dynamic theming disabled")
}
return
}
if (extractionRequested) {
fileChecker.running = true
}
}
}
Process {
id: fileChecker
command: ["test", "-r", wallpaperPath]
onExited: code => {
if (code === 0) {
matugenProcess.running = true
} else {
if (typeof ToastService !== "undefined") {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Wallpaper processing failed")
}
}
}
}
Process {
id: matugenProcess
command: ["matugen", "image", wallpaperPath, "--json", "hex"]
stdout: StdioCollector {
id: matugenCollector
onStreamFinished: {
if (!matugenCollector.text) {
if (typeof ToastService !== "undefined") {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Wallpaper Processing Failed: Empty JSON extracted from matugen output.")
}
return
}
const extractedJson = extractJsonFromText(matugenCollector.text)
if (!extractedJson) {
if (typeof ToastService !== "undefined") {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Wallpaper Processing Failed: Invalid JSON extracted from matugen output.")
}
console.log("Raw matugen output:", matugenCollector.text)
return
}
try {
root.matugenColors = JSON.parse(extractedJson)
root.colorUpdateTrigger++
generateAppConfigs()
if (typeof ToastService !== "undefined") {
ToastService.clearWallpaperError()
}
} catch (e) {
if (typeof ToastService !== "undefined") {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Wallpaper processing failed (JSON parse error after extraction)")
}
}
}
}
onExited: code => {
if (code !== 0) {
if (typeof ToastService !== "undefined") {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Matugen command failed with exit code " + code)
}
}
}
}
Process {
id: systemThemeGenerator
running: false
stdout: StdioCollector {
id: systemThemeStdout
}
stderr: StdioCollector {
id: systemThemeStderr
}
onExited: exitCode => {
systemThemeGenerationInProgress = false
if (exitCode !== 0) {
if (typeof ToastService !== "undefined") {
ToastService.showError("Failed to generate system themes: " + systemThemeStderr.text)
}
}
}
}
Process {
id: gtkApplier
running: false
stdout: StdioCollector {
id: gtkStdout
}
stderr: StdioCollector {
id: gtkStderr
}
onExited: exitCode => {
if (exitCode === 0) {
if (typeof ToastService !== "undefined") {
ToastService.showInfo("GTK colors applied successfully")
}
} else {
if (typeof ToastService !== "undefined") {
ToastService.showError("Failed to apply GTK colors: " + gtkStderr.text)
}
}
}
}
Process {
id: qtApplier
running: false
stdout: StdioCollector {
id: qtStdout
}
stderr: StdioCollector {
id: qtStderr
}
onExited: exitCode => {
if (exitCode === 0) {
if (typeof ToastService !== "undefined") {
ToastService.showInfo("Qt colors applied successfully")
}
} else {
if (typeof ToastService !== "undefined") {
ToastService.showError("Failed to apply Qt colors: " + qtStderr.text)
}
}
}
}
function generateAppConfigs() {
if (!matugenColors || !matugenColors.colors) {
return
}
generateSystemThemes()
}
Component.onCompleted: {
matugenCheck.running = true
if (typeof SessionData !== "undefined")
SessionData.isLightModeChanged.connect(root.onLightModeChanged)
// Generate system themes on startup for current theme
Qt.callLater(generateSystemThemesFromCurrentTheme)
}
FileView {
id: customThemeFileView
watchChanges: currentTheme === "custom"
function parseAndLoadTheme() {
try {
var themeData = JSON.parse(customThemeFileView.text())
loadCustomTheme(themeData)
} catch (e) {
ToastService.showError("Invalid JSON format: " + e.message)
}
}
onLoaded: {
parseAndLoadTheme()
}
onFileChanged: {
customThemeFileView.reload()
}
onLoadFailed: function(error) {
if (typeof ToastService !== "undefined") {
ToastService.showError("Failed to read theme file: " + error)
}
}
}
}
-678
View File
@@ -1,678 +0,0 @@
.pragma library
var single = (search, target) => {
if(!search || !target) return NULL
var preparedSearch = getPreparedSearch(search)
if(!isPrepared(target)) target = getPrepared(target)
var searchBitflags = preparedSearch.bitflags
if((searchBitflags & target._bitflags) !== searchBitflags) return NULL
return algorithm(preparedSearch, target)
}
var go = (search, targets, options) => {
if(!search) return options?.all ? all(targets, options) : noResults
var preparedSearch = getPreparedSearch(search)
var searchBitflags = preparedSearch.bitflags
var containsSpace = preparedSearch.containsSpace
var threshold = denormalizeScore( options?.threshold || 0 )
var limit = options?.limit || INFINITY
var resultsLen = 0; var limitedCount = 0
var targetsLen = targets.length
function push_result(result) {
if(resultsLen < limit) { q.add(result); ++resultsLen }
else {
++limitedCount
if(result._score > q.peek()._score) q.replaceTop(result)
}
}
// This code is copy/pasted 3 times for performance reasons [options.key, options.keys, no keys]
// options.key
if(options?.key) {
var key = options.key
for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]
var target = getValue(obj, key)
if(!target) continue
if(!isPrepared(target)) target = getPrepared(target)
if((searchBitflags & target._bitflags) !== searchBitflags) continue
var result = algorithm(preparedSearch, target)
if(result === NULL) continue
if(result._score < threshold) continue
result.obj = obj
push_result(result)
}
// options.keys
} else if(options?.keys) {
var keys = options.keys
var keysLen = keys.length
outer: for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]
{ // early out based on bitflags
var keysBitflags = 0
for (var keyI = 0; keyI < keysLen; ++keyI) {
var key = keys[keyI]
var target = getValue(obj, key)
if(!target) { tmpTargets[keyI] = noTarget; continue }
if(!isPrepared(target)) target = getPrepared(target)
tmpTargets[keyI] = target
keysBitflags |= target._bitflags
}
if((searchBitflags & keysBitflags) !== searchBitflags) continue
}
if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) keysSpacesBestScores[i] = NEGATIVE_INFINITY
for (var keyI = 0; keyI < keysLen; ++keyI) {
target = tmpTargets[keyI]
if(target === noTarget) { tmpResults[keyI] = noTarget; continue }
tmpResults[keyI] = algorithm(preparedSearch, target, /*allowSpaces=*/false, /*allowPartialMatch=*/containsSpace)
if(tmpResults[keyI] === NULL) { tmpResults[keyI] = noTarget; continue }
// todo: this seems weird and wrong. like what if our first match wasn't good. this should just replace it instead of averaging with it
// if our second match isn't good we ignore it instead of averaging with it
if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) {
if(allowPartialMatchScores[i] > -1000) {
if(keysSpacesBestScores[i] > NEGATIVE_INFINITY) {
var tmp = (keysSpacesBestScores[i] + allowPartialMatchScores[i]) / 4/*bonus score for having multiple matches*/
if(tmp > keysSpacesBestScores[i]) keysSpacesBestScores[i] = tmp
}
}
if(allowPartialMatchScores[i] > keysSpacesBestScores[i]) keysSpacesBestScores[i] = allowPartialMatchScores[i]
}
}
if(containsSpace) {
for(let i=0; i<preparedSearch.spaceSearches.length; i++) { if(keysSpacesBestScores[i] === NEGATIVE_INFINITY) continue outer }
} else {
var hasAtLeast1Match = false
for(let i=0; i < keysLen; i++) { if(tmpResults[i]._score !== NEGATIVE_INFINITY) { hasAtLeast1Match = true; break } }
if(!hasAtLeast1Match) continue
}
var objResults = new KeysResult(keysLen)
for(let i=0; i < keysLen; i++) { objResults[i] = tmpResults[i] }
if(containsSpace) {
var score = 0
for(let i=0; i<preparedSearch.spaceSearches.length; i++) score += keysSpacesBestScores[i]
} else {
// todo could rewrite this scoring to be more similar to when there's spaces
// if we match multiple keys give us bonus points
var score = NEGATIVE_INFINITY
for(let i=0; i<keysLen; i++) {
var result = objResults[i]
if(result._score > -1000) {
if(score > NEGATIVE_INFINITY) {
var tmp = (score + result._score) / 4/*bonus score for having multiple matches*/
if(tmp > score) score = tmp
}
}
if(result._score > score) score = result._score
}
}
objResults.obj = obj
objResults._score = score
if(options?.scoreFn) {
score = options.scoreFn(objResults)
if(!score) continue
score = denormalizeScore(score)
objResults._score = score
}
if(score < threshold) continue
push_result(objResults)
}
// no keys
} else {
for(var i = 0; i < targetsLen; ++i) { var target = targets[i]
if(!target) continue
if(!isPrepared(target)) target = getPrepared(target)
if((searchBitflags & target._bitflags) !== searchBitflags) continue
var result = algorithm(preparedSearch, target)
if(result === NULL) continue
if(result._score < threshold) continue
push_result(result)
}
}
if(resultsLen === 0) return noResults
var results = new Array(resultsLen)
for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll()
results.total = resultsLen + limitedCount
return results
}
// this is written as 1 function instead of 2 for minification. perf seems fine ...
// except when minified. the perf is very slow
var highlight = (result, open='<b>', close='</b>') => {
var callback = typeof open === 'function' ? open : undefined
var target = result.target
var targetLen = target.length
var indexes = result.indexes
var highlighted = ''
var matchI = 0
var indexesI = 0
var opened = false
var parts = []
for(var i = 0; i < targetLen; ++i) { var char = target[i]
if(indexes[indexesI] === i) {
++indexesI
if(!opened) { opened = true
if(callback) {
parts.push(highlighted); highlighted = ''
} else {
highlighted += open
}
}
if(indexesI === indexes.length) {
if(callback) {
highlighted += char
parts.push(callback(highlighted, matchI++)); highlighted = ''
parts.push(target.substr(i+1))
} else {
highlighted += char + close + target.substr(i+1)
}
break
}
} else {
if(opened) { opened = false
if(callback) {
parts.push(callback(highlighted, matchI++)); highlighted = ''
} else {
highlighted += close
}
}
}
highlighted += char
}
return callback ? parts : highlighted
}
var prepare = (target) => {
if(typeof target === 'number') target = ''+target
else if(typeof target !== 'string') target = ''
var info = prepareLowerInfo(target)
return new_result(target, {_targetLower:info._lower, _targetLowerCodes:info.lowerCodes, _bitflags:info.bitflags})
}
var cleanup = () => { preparedCache.clear(); preparedSearchCache.clear() }
// Below this point is only internal code
// Below this point is only internal code
// Below this point is only internal code
// Below this point is only internal code
class Result {
get ['indexes']() { return this._indexes.slice(0, this._indexes.len).sort((a,b)=>a-b) }
set ['indexes'](indexes) { return this._indexes = indexes }
['highlight'](open, close) { return highlight(this, open, close) }
get ['score']() { return normalizeScore(this._score) }
set ['score'](score) { this._score = denormalizeScore(score) }
}
class KeysResult extends Array {
get ['score']() { return normalizeScore(this._score) }
set ['score'](score) { this._score = denormalizeScore(score) }
}
var new_result = (target, options) => {
const result = new Result()
result['target'] = target
result['obj'] = options.obj ?? NULL
result._score = options._score ?? NEGATIVE_INFINITY
result._indexes = options._indexes ?? []
result._targetLower = options._targetLower ?? ''
result._targetLowerCodes = options._targetLowerCodes ?? NULL
result._nextBeginningIndexes = options._nextBeginningIndexes ?? NULL
result._bitflags = options._bitflags ?? 0
return result
}
var normalizeScore = score => {
if(score === NEGATIVE_INFINITY) return 0
if(score > 1) return score
return Math.E ** ( ((-score + 1)**.04307 - 1) * -2)
}
var denormalizeScore = normalizedScore => {
if(normalizedScore === 0) return NEGATIVE_INFINITY
if(normalizedScore > 1) return normalizedScore
return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307)
}
var prepareSearch = (search) => {
if(typeof search === 'number') search = ''+search
else if(typeof search !== 'string') search = ''
search = search.trim()
var info = prepareLowerInfo(search)
var spaceSearches = []
if(info.containsSpace) {
var searches = search.split(/\s+/)
searches = [...new Set(searches)] // distinct
for(var i=0; i<searches.length; i++) {
if(searches[i] === '') continue
var _info = prepareLowerInfo(searches[i])
spaceSearches.push({lowerCodes:_info.lowerCodes, _lower:searches[i].toLowerCase(), containsSpace:false})
}
}
return {lowerCodes: info.lowerCodes, _lower: info._lower, containsSpace: info.containsSpace, bitflags: info.bitflags, spaceSearches: spaceSearches}
}
var getPrepared = (target) => {
if(target.length > 999) return prepare(target) // don't cache huge targets
var targetPrepared = preparedCache.get(target)
if(targetPrepared !== undefined) return targetPrepared
targetPrepared = prepare(target)
preparedCache.set(target, targetPrepared)
return targetPrepared
}
var getPreparedSearch = (search) => {
if(search.length > 999) return prepareSearch(search) // don't cache huge searches
var searchPrepared = preparedSearchCache.get(search)
if(searchPrepared !== undefined) return searchPrepared
searchPrepared = prepareSearch(search)
preparedSearchCache.set(search, searchPrepared)
return searchPrepared
}
var all = (targets, options) => {
var results = []; results.total = targets.length // this total can be wrong if some targets are skipped
var limit = options?.limit || INFINITY
if(options?.key) {
for(var i=0;i<targets.length;i++) { var obj = targets[i]
var target = getValue(obj, options.key)
if(target == NULL) continue
if(!isPrepared(target)) target = getPrepared(target)
var result = new_result(target.target, {_score: target._score, obj: obj})
results.push(result); if(results.length >= limit) return results
}
} else if(options?.keys) {
for(var i=0;i<targets.length;i++) { var obj = targets[i]
var objResults = new KeysResult(options.keys.length)
for (var keyI = options.keys.length - 1; keyI >= 0; --keyI) {
var target = getValue(obj, options.keys[keyI])
if(!target) { objResults[keyI] = noTarget; continue }
if(!isPrepared(target)) target = getPrepared(target)
target._score = NEGATIVE_INFINITY
target._indexes.len = 0
objResults[keyI] = target
}
objResults.obj = obj
objResults._score = NEGATIVE_INFINITY
results.push(objResults); if(results.length >= limit) return results
}
} else {
for(var i=0;i<targets.length;i++) { var target = targets[i]
if(target == NULL) continue
if(!isPrepared(target)) target = getPrepared(target)
target._score = NEGATIVE_INFINITY
target._indexes.len = 0
results.push(target); if(results.length >= limit) return results
}
}
return results
}
var algorithm = (preparedSearch, prepared, allowSpaces=false, allowPartialMatch=false) => {
if(allowSpaces===false && preparedSearch.containsSpace) return algorithmSpaces(preparedSearch, prepared, allowPartialMatch)
var searchLower = preparedSearch._lower
var searchLowerCodes = preparedSearch.lowerCodes
var searchLowerCode = searchLowerCodes[0]
var targetLowerCodes = prepared._targetLowerCodes
var searchLen = searchLowerCodes.length
var targetLen = targetLowerCodes.length
var searchI = 0 // where we at
var targetI = 0 // where you at
var matchesSimpleLen = 0
// very basic fuzzy match; to remove non-matching targets ASAP!
// walk through target. find sequential matches.
// if all chars aren't found then exit
for(;;) {
var isMatch = searchLowerCode === targetLowerCodes[targetI]
if(isMatch) {
matchesSimple[matchesSimpleLen++] = targetI
++searchI; if(searchI === searchLen) break
searchLowerCode = searchLowerCodes[searchI]
}
++targetI; if(targetI >= targetLen) return NULL // Failed to find searchI
}
var searchI = 0
var successStrict = false
var matchesStrictLen = 0
var nextBeginningIndexes = prepared._nextBeginningIndexes
if(nextBeginningIndexes === NULL) nextBeginningIndexes = prepared._nextBeginningIndexes = prepareNextBeginningIndexes(prepared.target)
targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1]
// Our target string successfully matched all characters in sequence!
// Let's try a more advanced and strict test to improve the score
// only count it as a match if it's consecutive or a beginning character!
var backtrackCount = 0
if(targetI !== targetLen) for(;;) {
if(targetI >= targetLen) {
// We failed to find a good spot for this search char, go back to the previous search char and force it forward
if(searchI <= 0) break // We failed to push chars forward for a better match
++backtrackCount; if(backtrackCount > 200) break // exponential backtracking is taking too long, just give up and return a bad match
--searchI
var lastMatch = matchesStrict[--matchesStrictLen]
targetI = nextBeginningIndexes[lastMatch]
} else {
var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]
if(isMatch) {
matchesStrict[matchesStrictLen++] = targetI
++searchI; if(searchI === searchLen) { successStrict = true; break }
++targetI
} else {
targetI = nextBeginningIndexes[targetI]
}
}
}
// check if it's a substring match
var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, matchesSimple[0]) // perf: this is slow
var isSubstring = !!~substringIndex
var isSubstringBeginning = !isSubstring ? false : substringIndex===0 || prepared._nextBeginningIndexes[substringIndex-1] === substringIndex
// if it's a substring match but not at a beginning index, let's try to find a substring starting at a beginning index for a better score
if(isSubstring && !isSubstringBeginning) {
for(var i=0; i<nextBeginningIndexes.length; i=nextBeginningIndexes[i]) {
if(i <= substringIndex) continue
for(var s=0; s<searchLen; s++) if(searchLowerCodes[s] !== prepared._targetLowerCodes[i+s]) break
if(s === searchLen) { substringIndex = i; isSubstringBeginning = true; break }
}
}
// tally up the score & keep track of matches for highlighting later
// if it's a simple match, we'll switch to a substring match if a substring exists
// if it's a strict match, we'll switch to a substring match only if that's a better score
var calculateScore = matches => {
var score = 0
var extraMatchGroupCount = 0
for(var i = 1; i < searchLen; ++i) {
if(matches[i] - matches[i-1] !== 1) {score -= matches[i]; ++extraMatchGroupCount}
}
var unmatchedDistance = matches[searchLen-1] - matches[0] - (searchLen-1)
score -= (12+unmatchedDistance) * extraMatchGroupCount // penality for more groups
if(matches[0] !== 0) score -= matches[0]*matches[0]*.2 // penality for not starting near the beginning
if(!successStrict) {
score *= 1000
} else {
// successStrict on a target with too many beginning indexes loses points for being a bad target
var uniqueBeginningIndexes = 1
for(var i = nextBeginningIndexes[0]; i < targetLen; i=nextBeginningIndexes[i]) ++uniqueBeginningIndexes
if(uniqueBeginningIndexes > 24) score *= (uniqueBeginningIndexes-24)*10 // quite arbitrary numbers here ...
}
score -= (targetLen - searchLen)/2 // penality for longer targets
if(isSubstring) score /= 1+searchLen*searchLen*1 // bonus for being a full substring
if(isSubstringBeginning) score /= 1+searchLen*searchLen*1 // bonus for substring starting on a beginningIndex
score -= (targetLen - searchLen)/2 // penality for longer targets
return score
}
if(!successStrict) {
if(isSubstring) for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches
var matchesBest = matchesSimple
var score = calculateScore(matchesBest)
} else {
if(isSubstringBeginning) {
for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches
var matchesBest = matchesSimple
var score = calculateScore(matchesSimple)
} else {
var matchesBest = matchesStrict
var score = calculateScore(matchesStrict)
}
}
prepared._score = score
for(var i = 0; i < searchLen; ++i) prepared._indexes[i] = matchesBest[i]
prepared._indexes.len = searchLen
const result = new Result()
result.target = prepared.target
result._score = prepared._score
result._indexes = prepared._indexes
return result
}
var algorithmSpaces = (preparedSearch, target, allowPartialMatch) => {
var seen_indexes = new Set()
var score = 0
var result = NULL
var first_seen_index_last_search = 0
var searches = preparedSearch.spaceSearches
var searchesLen = searches.length
var changeslen = 0
// Return _nextBeginningIndexes back to its normal state
var resetNextBeginningIndexes = () => {
for(let i=changeslen-1; i>=0; i--) target._nextBeginningIndexes[nextBeginningIndexesChanges[i*2 + 0]] = nextBeginningIndexesChanges[i*2 + 1]
}
var hasAtLeast1Match = false
for(var i=0; i<searchesLen; ++i) {
allowPartialMatchScores[i] = NEGATIVE_INFINITY
var search = searches[i]
result = algorithm(search, target)
if(allowPartialMatch) {
if(result === NULL) continue
hasAtLeast1Match = true
} else {
if(result === NULL) {resetNextBeginningIndexes(); return NULL}
}
// if not the last search, we need to mutate _nextBeginningIndexes for the next search
var isTheLastSearch = i === searchesLen - 1
if(!isTheLastSearch) {
var indexes = result._indexes
var indexesIsConsecutiveSubstring = true
for(let i=0; i<indexes.len-1; i++) {
if(indexes[i+1] - indexes[i] !== 1) {
indexesIsConsecutiveSubstring = false; break;
}
}
if(indexesIsConsecutiveSubstring) {
var newBeginningIndex = indexes[indexes.len-1] + 1
var toReplace = target._nextBeginningIndexes[newBeginningIndex-1]
for(let i=newBeginningIndex-1; i>=0; i--) {
if(toReplace !== target._nextBeginningIndexes[i]) break
target._nextBeginningIndexes[i] = newBeginningIndex
nextBeginningIndexesChanges[changeslen*2 + 0] = i
nextBeginningIndexesChanges[changeslen*2 + 1] = toReplace
changeslen++
}
}
}
score += result._score / searchesLen
allowPartialMatchScores[i] = result._score / searchesLen
// dock points based on order otherwise "c man" returns Manifest.cpp instead of CheatManager.h
if(result._indexes[0] < first_seen_index_last_search) {
score -= (first_seen_index_last_search - result._indexes[0]) * 2
}
first_seen_index_last_search = result._indexes[0]
for(var j=0; j<result._indexes.len; ++j) seen_indexes.add(result._indexes[j])
}
if(allowPartialMatch && !hasAtLeast1Match) return NULL
resetNextBeginningIndexes()
// allows a search with spaces that's an exact substring to score well
var allowSpacesResult = algorithm(preparedSearch, target, /*allowSpaces=*/true)
if(allowSpacesResult !== NULL && allowSpacesResult._score > score) {
if(allowPartialMatch) {
for(var i=0; i<searchesLen; ++i) {
allowPartialMatchScores[i] = allowSpacesResult._score / searchesLen
}
}
return allowSpacesResult
}
if(allowPartialMatch) result = target
result._score = score
var i = 0
for (let index of seen_indexes) result._indexes[i++] = index
result._indexes.len = i
return result
}
// we use this instead of just .normalize('NFD').replace(/[\u0300-\u036f]/g, '') because that screws with japanese characters
var remove_accents = (str) => str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, '')
var prepareLowerInfo = (str) => {
str = remove_accents(str)
var strLen = str.length
var lower = str.toLowerCase()
var lowerCodes = [] // new Array(strLen) sparse array is too slow
var bitflags = 0
var containsSpace = false // space isn't stored in bitflags because of how searching with a space works
for(var i = 0; i < strLen; ++i) {
var lowerCode = lowerCodes[i] = lower.charCodeAt(i)
if(lowerCode === 32) {
containsSpace = true
continue // it's important that we don't set any bitflags for space
}
var bit = lowerCode>=97&&lowerCode<=122 ? lowerCode-97 // alphabet
: lowerCode>=48&&lowerCode<=57 ? 26 // numbers
// 3 bits available
: lowerCode<=127 ? 30 // other ascii
: 31 // other utf8
bitflags |= 1<<bit
}
return {lowerCodes:lowerCodes, bitflags:bitflags, containsSpace:containsSpace, _lower:lower}
}
var prepareBeginningIndexes = (target) => {
var targetLen = target.length
var beginningIndexes = []; var beginningIndexesLen = 0
var wasUpper = false
var wasAlphanum = false
for(var i = 0; i < targetLen; ++i) {
var targetCode = target.charCodeAt(i)
var isUpper = targetCode>=65&&targetCode<=90
var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57
var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum
wasUpper = isUpper
wasAlphanum = isAlphanum
if(isBeginning) beginningIndexes[beginningIndexesLen++] = i
}
return beginningIndexes
}
var prepareNextBeginningIndexes = (target) => {
target = remove_accents(target)
var targetLen = target.length
var beginningIndexes = prepareBeginningIndexes(target)
var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow
var lastIsBeginning = beginningIndexes[0]
var lastIsBeginningI = 0
for(var i = 0; i < targetLen; ++i) {
if(lastIsBeginning > i) {
nextBeginningIndexes[i] = lastIsBeginning
} else {
lastIsBeginning = beginningIndexes[++lastIsBeginningI]
nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning
}
}
return nextBeginningIndexes
}
var preparedCache = new Map()
var preparedSearchCache = new Map()
// the theory behind these being globals is to reduce garbage collection by not making new arrays
var matchesSimple = []; var matchesStrict = []
var nextBeginningIndexesChanges = [] // allows straw berry to match strawberry well, by modifying the end of a substring to be considered a beginning index for the rest of the search
var keysSpacesBestScores = []; var allowPartialMatchScores = []
var tmpTargets = []; var tmpResults = []
// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop]
// prop = 'key1.key2' 10ms
// prop = ['key1', 'key2'] 27ms
// prop = obj => obj.tags.join() ??ms
var getValue = (obj, prop) => {
var tmp = obj[prop]; if(tmp !== undefined) return tmp
if(typeof prop === 'function') return prop(obj) // this should run first. but that makes string props slower
var segs = prop
if(!Array.isArray(prop)) segs = prop.split('.')
var len = segs.length
var i = -1
while (obj && (++i < len)) obj = obj[segs[i]]
return obj
}
var isPrepared = (x) => { return typeof x === 'object' && typeof x._bitflags === 'number' }
var INFINITY = Infinity; var NEGATIVE_INFINITY = -INFINITY
var noResults = []; noResults.total = 0
var NULL = null
var noTarget = prepare('')
// Hacked version of https://github.com/lemire/FastPriorityQueue.js
var fastpriorityqueue=r=>{var e=[],o=0,a={},v=r=>{for(var a=0,v=e[a],c=1;c<o;){var s=c+1;a=c,s<o&&e[s]._score<e[c]._score&&(a=s),e[a-1>>1]=e[a],c=1+(a<<1)}for(var f=a-1>>1;a>0&&v._score<e[f]._score;f=(a=f)-1>>1)e[a]=e[f];e[a]=v};return a.add=(r=>{var a=o;e[o++]=r;for(var v=a-1>>1;a>0&&r._score<e[v]._score;v=(a=v)-1>>1)e[a]=e[v];e[a]=r}),a.poll=(r=>{if(0!==o){var a=e[0];return e[0]=e[--o],v(),a}}),a.peek=(r=>{if(0!==o)return e[0]}),a.replaceTop=(r=>{e[0]=r,v()}),a}
var q = fastpriorityqueue() // reuse this
-106
View File
@@ -1,106 +0,0 @@
.pragma library
// This exists only beacause I haven't been able to get linkColor to work with MarkdownText
// May not be necessary if that's possible tbh.
function markdownToHtml(text) {
if (!text) return "";
// Store code blocks and inline code to protect them from further processing
const codeBlocks = [];
const inlineCode = [];
let blockIndex = 0;
let inlineIndex = 0;
// First, extract and replace code blocks with placeholders
let html = text.replace(/```([\s\S]*?)```/g, (match, code) => {
// Trim leading and trailing blank lines only
const trimmedCode = code.replace(/^\n+|\n+$/g, '');
// Escape HTML entities in code
const escapedCode = trimmedCode.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
codeBlocks.push(`<pre><code>${escapedCode}</code></pre>`);
return `\x00CODEBLOCK${blockIndex++}\x00`;
});
// Extract and replace inline code
html = html.replace(/`([^`]+)`/g, (match, code) => {
// Escape HTML entities in code
const escapedCode = code.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
inlineCode.push(`<code>${escapedCode}</code>`);
return `\x00INLINECODE${inlineIndex++}\x00`;
});
// Now process everything else
// Escape HTML entities (but not in code blocks)
html = html.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// Headers
html = html.replace(/^### (.*?)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.*?)$/gm, '<h2>$1</h2>');
html = html.replace(/^# (.*?)$/gm, '<h1>$1</h1>');
// Bold and italic (order matters!)
html = html.replace(/\*\*\*(.*?)\*\*\*/g, '<b><i>$1</i></b>');
html = html.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
html = html.replace(/\*(.*?)\*/g, '<i>$1</i>');
html = html.replace(/___(.*?)___/g, '<b><i>$1</i></b>');
html = html.replace(/__(.*?)__/g, '<b>$1</b>');
html = html.replace(/_(.*?)_/g, '<i>$1</i>');
// Links
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
// Lists
html = html.replace(/^\* (.*?)$/gm, '<li>$1</li>');
html = html.replace(/^- (.*?)$/gm, '<li>$1</li>');
html = html.replace(/^\d+\. (.*?)$/gm, '<li>$1</li>');
// Wrap consecutive list items in ul/ol tags
html = html.replace(/(<li>[\s\S]*?<\/li>\s*)+/g, function(match) {
return '<ul>' + match + '</ul>';
});
// Detect plain URLs and wrap them in anchor tags (but not inside existing <a> or markdown links)
html = html.replace(/(^|[^"'>])((https?|file):\/\/[^\s<]+)/g, '$1<a href="$2">$2</a>');
// Restore code blocks and inline code BEFORE line break processing
html = html.replace(/\x00CODEBLOCK(\d+)\x00/g, (match, index) => {
return codeBlocks[parseInt(index)];
});
html = html.replace(/\x00INLINECODE(\d+)\x00/g, (match, index) => {
return inlineCode[parseInt(index)];
});
// Line breaks (after code blocks are restored)
html = html.replace(/\n\n/g, '</p><p>');
html = html.replace(/\n/g, '<br/>');
// Wrap in paragraph tags if not already wrapped
if (!html.startsWith('<')) {
html = '<p>' + html + '</p>';
}
// Clean up the final HTML
// Remove <br/> tags immediately before block elements
html = html.replace(/<br\/>\s*<pre>/g, '<pre>');
html = html.replace(/<br\/>\s*<ul>/g, '<ul>');
html = html.replace(/<br\/>\s*<h[1-6]>/g, '<h$1>');
// Remove empty paragraphs
html = html.replace(/<p>\s*<\/p>/g, '');
html = html.replace(/<p>\s*<br\/>\s*<\/p>/g, '');
// Remove excessive line breaks
html = html.replace(/(<br\/>){3,}/g, '<br/><br/>'); // Max 2 consecutive line breaks
html = html.replace(/(<\/p>)\s*(<p>)/g, '$1$2'); // Remove whitespace between paragraphs
// Remove leading/trailing whitespace
html = html.trim();
return html;
}
-674
View File
@@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
-904
View File
@@ -1,904 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
DankModal {
id: clipboardHistoryModal
property int totalCount: 0
property var activeTheme: Theme
property bool showClearConfirmation: false
property var clipboardEntries: []
property string searchText: ""
property int selectedIndex: 0
property bool keyboardNavigationActive: false
property bool showKeyboardHints: false
property Component clipboardContent
function updateFilteredModel() {
filteredClipboardModel.clear()
for (var i = 0; i < clipboardModel.count; i++) {
const entry = clipboardModel.get(i).entry
if (searchText.trim().length === 0) {
filteredClipboardModel.append({
"entry": entry
})
} else {
const content = getEntryPreview(entry).toLowerCase()
if (content.includes(searchText.toLowerCase()))
filteredClipboardModel.append({
"entry": entry
})
}
}
clipboardHistoryModal.totalCount = filteredClipboardModel.count
// Clamp selectedIndex to valid range
if (filteredClipboardModel.count === 0) {
keyboardNavigationActive = false
selectedIndex = 0
} else if (selectedIndex >= filteredClipboardModel.count) {
selectedIndex = filteredClipboardModel.count - 1
}
}
function toggle() {
if (shouldBeVisible)
hide()
else
show()
}
function show() {
open()
clipboardHistoryModal.searchText = ""
initializeThumbnailSystem()
refreshClipboard()
keyboardController.reset()
Qt.callLater(function () {
if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.text = ""
contentLoader.item.searchField.forceActiveFocus()
}
})
}
function hide() {
close()
clipboardHistoryModal.searchText = ""
updateFilteredModel()
keyboardController.reset()
cleanupTempFiles()
}
function initializeThumbnailSystem() {}
function cleanupTempFiles() {
Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"])
}
function generateThumbnails() {}
function refreshClipboard() {
clipboardProcess.running = true
}
function copyEntry(entry) {
const entryId = entry.split('\t')[0]
Quickshell.execDetached(
["sh", "-c", `cliphist decode ${entryId} | wl-copy`])
ToastService.showInfo("Copied to clipboard")
hide()
}
function deleteEntry(entry) {
deleteProcess.deletedEntry = entry
deleteProcess.command = ["sh", "-c", `echo '${entry.replace(
/'/g, "'\\''")}' | cliphist delete`]
deleteProcess.running = true
}
function clearAll() {
clearProcess.running = true
}
function getEntryPreview(entry) {
let content = entry.replace(/^\s*\d+\s+/, "")
if (content.includes("image/") || content.includes("binary data")
|| /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) {
const dimensionMatch = content.match(/(\d+)x(\d+)/)
if (dimensionMatch)
return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}`
const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i)
if (typeMatch)
return `Image (${typeMatch[1].toUpperCase()})`
return "Image"
}
if (content.length > 100)
return content.substring(0, 100) + "..."
return content
}
function getEntryType(entry) {
if (entry.includes("image/") || entry.includes("binary data")
|| /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry)
|| /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry))
return "image"
if (entry.length > 200)
return "long_text"
return "text"
}
visible: false
width: 650
height: 550
backgroundColor: Theme.popupBackground()
cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium
borderWidth: 1
enableShadow: true
onBackgroundClicked: {
hide()
}
modalFocusScope.Keys.onPressed: function (event) {
keyboardController.handleKey(event)
}
content: clipboardContent
QtObject {
id: keyboardController
function reset() {
selectedIndex = 0
keyboardNavigationActive = false
showKeyboardHints = false
if (typeof clipboardListView !== 'undefined' && clipboardListView)
clipboardListView.keyboardActive = false
}
function selectNext() {
if (filteredClipboardModel.count === 0)
return
keyboardNavigationActive = true
selectedIndex = Math.min(selectedIndex + 1,
filteredClipboardModel.count - 1)
}
function selectPrevious() {
if (filteredClipboardModel.count === 0)
return
keyboardNavigationActive = true
selectedIndex = Math.max(selectedIndex - 1, 0)
}
function copySelected() {
if (filteredClipboardModel.count === 0 || selectedIndex < 0
|| selectedIndex >= filteredClipboardModel.count)
return
var selectedEntry = filteredClipboardModel.get(selectedIndex).entry
copyEntry(selectedEntry)
}
function deleteSelected() {
if (filteredClipboardModel.count === 0 || selectedIndex < 0
|| selectedIndex >= filteredClipboardModel.count)
return
var selectedEntry = filteredClipboardModel.get(selectedIndex).entry
deleteEntry(selectedEntry)
}
function handleKey(event) {
if (event.key === Qt.Key_Escape) {
if (keyboardNavigationActive) {
keyboardNavigationActive = false
if (typeof clipboardListView !== 'undefined'
&& clipboardListView)
clipboardListView.keyboardActive = false
event.accepted = true
} else {
hide()
event.accepted = true
}
} else if (event.key === Qt.Key_Down) {
if (!keyboardNavigationActive) {
keyboardNavigationActive = true
selectedIndex = 0
if (typeof clipboardListView !== 'undefined'
&& clipboardListView)
clipboardListView.keyboardActive = true
event.accepted = true
} else {
selectNext()
event.accepted = true
}
} else if (event.key === Qt.Key_Up) {
if (!keyboardNavigationActive) {
keyboardNavigationActive = true
selectedIndex = 0
if (typeof clipboardListView !== 'undefined'
&& clipboardListView)
clipboardListView.keyboardActive = true
event.accepted = true
} else if (selectedIndex === 0) {
keyboardNavigationActive = false
if (typeof clipboardListView !== 'undefined'
&& clipboardListView)
clipboardListView.keyboardActive = false
event.accepted = true
} else {
selectPrevious()
event.accepted = true
}
} else if (event.key === Qt.Key_Delete
&& (event.modifiers & Qt.ShiftModifier)) {
clearAll()
hide()
event.accepted = true
} else if (keyboardNavigationActive) {
if ((event.key === Qt.Key_C
&& (event.modifiers & Qt.ControlModifier))
|| event.key === Qt.Key_Return
|| event.key === Qt.Key_Enter) {
copySelected()
event.accepted = true
} else if (event.key === Qt.Key_Delete) {
deleteSelected()
event.accepted = true
}
}
if (event.key === Qt.Key_F10) {
showKeyboardHints = !showKeyboardHints
event.accepted = true
}
}
}
DankModal {
id: clearConfirmDialog
visible: showClearConfirmation
width: 350
height: 150
onBackgroundClicked: {
showClearConfirmation = false
}
content: Component {
Item {
anchors.fill: parent
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingM
StyledText {
text: "Clear All History?"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "This will permanently delete all clipboard history."
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: cancelClearButton.containsMouse ? Theme.surfaceTextPressed : Theme.surfaceVariantAlpha
StyledText {
text: "Cancel"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: cancelClearButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: showClearConfirmation = false
}
}
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: confirmClearButton.containsMouse ? Theme.errorPressed : Theme.error
StyledText {
text: "Clear All"
font.pixelSize: Theme.fontSizeMedium
color: Theme.primaryText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: confirmClearButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
clearAll()
showClearConfirmation = false
hide()
}
}
}
}
}
}
}
}
ListModel {
id: clipboardModel
}
ListModel {
id: filteredClipboardModel
}
Process {
id: clipboardProcess
command: ["cliphist", "list"]
running: false
stdout: StdioCollector {
onStreamFinished: {
clipboardModel.clear()
const lines = text.trim().split('\n')
for (const line of lines) {
if (line.trim().length > 0)
clipboardModel.append({
"entry": line
})
}
updateFilteredModel()
}
}
}
Process {
id: deleteProcess
property string deletedEntry: ""
running: false
onExited: exitCode => {
if (exitCode === 0) {
// Just remove the item from models instead of re-fetching everything
for (var i = 0; i < clipboardModel.count; i++) {
if (clipboardModel.get(
i).entry === deleteProcess.deletedEntry) {
clipboardModel.remove(i)
break
}
}
for (var j = 0; j < filteredClipboardModel.count; j++) {
if (filteredClipboardModel.get(
j).entry === deleteProcess.deletedEntry) {
filteredClipboardModel.remove(j)
break
}
}
clipboardHistoryModal.totalCount = filteredClipboardModel.count
// Clamp selectedIndex to valid range
if (filteredClipboardModel.count === 0) {
keyboardNavigationActive = false
selectedIndex = 0
} else if (selectedIndex >= filteredClipboardModel.count) {
selectedIndex = filteredClipboardModel.count - 1
}
} else {
console.warn("Failed to delete clipboard entry")
}
}
}
Process {
id: clearProcess
command: ["cliphist", "wipe"]
running: false
onExited: exitCode => {
if (exitCode === 0) {
clipboardModel.clear()
filteredClipboardModel.clear()
totalCount = 0
} else {
}
}
}
IpcHandler {
function open() {
clipboardHistoryModal.show()
return "CLIPBOARD_OPEN_SUCCESS"
}
function close() {
hide()
return "CLIPBOARD_CLOSE_SUCCESS"
}
function toggle() {
clipboardHistoryModal.toggle()
return "CLIPBOARD_TOGGLE_SUCCESS"
}
target: "clipboard"
}
clipboardContent: Component {
Item {
id: clipboardContent
property alias searchField: searchField
anchors.fill: parent
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
focus: false
Item {
width: parent.width
height: 40
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "content_paste"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: `Clipboard History (${totalCount})`
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankActionButton {
iconName: "info"
iconSize: Theme.iconSize - 4
iconColor: showKeyboardHints ? Theme.primary : Theme.surfaceText
hoverColor: Theme.primaryHover
onClicked: {
showKeyboardHints = !showKeyboardHints
}
}
DankActionButton {
iconName: "delete_sweep"
iconSize: Theme.iconSize
iconColor: Theme.error
hoverColor: Theme.errorHover
onClicked: {
showClearConfirmation = true
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: hide()
}
}
}
DankTextField {
id: searchField
width: parent.width
placeholderText: ""
leftIconName: "search"
showClearButton: true
focus: true
ignoreLeftRightKeys: true
keyForwardTargets: [modalFocusScope]
onTextChanged: {
clipboardHistoryModal.searchText = text
updateFilteredModel()
}
Keys.onEscapePressed: function (event) {
hide()
event.accepted = true
}
Component.onCompleted: {
Qt.callLater(function () {
forceActiveFocus()
})
}
Connections {
target: clipboardHistoryModal
function onOpened() {
Qt.callLater(function () {
searchField.forceActiveFocus()
})
}
}
}
Rectangle {
width: parent.width
height: parent.height - 110
radius: Theme.cornerRadius
color: Theme.surfaceLight
border.color: Theme.outlineLight
border.width: 1
clip: true
DankListView {
id: clipboardListView
function ensureVisible(index) {
if (index < 0 || index >= count)
return
var itemHeight = 72 + spacing
var itemY = index * itemHeight
var itemBottom = itemY + itemHeight
if (itemY < contentY)
contentY = itemY
else if (itemBottom > contentY + height)
contentY = itemBottom - height
}
anchors.fill: parent
anchors.margins: Theme.spacingS
clip: true
model: filteredClipboardModel
currentIndex: selectedIndex
spacing: Theme.spacingXS
interactive: true
flickDeceleration: 1500
maximumFlickVelocity: 2000
boundsBehavior: Flickable.DragAndOvershootBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
onCurrentIndexChanged: {
if (keyboardNavigationActive && currentIndex >= 0)
ensureVisible(currentIndex)
}
StyledText {
text: "No clipboard entries found"
anchors.centerIn: parent
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: filteredClipboardModel.count === 0
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
delegate: Rectangle {
property string entryType: getEntryType(model.entry)
property string entryPreview: getEntryPreview(
model.entry)
property int entryIndex: index + 1
property string entryData: model.entry
property alias thumbnailImageSource: thumbnailImageSource
width: clipboardListView.width
height: 72
radius: Theme.cornerRadius
color: {
if (keyboardNavigationActive
&& index === selectedIndex)
return Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.2)
return mouseArea.containsMouse ? Theme.primaryHover : Theme.primaryBackground
}
border.color: {
if (keyboardNavigationActive
&& index === selectedIndex)
return Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b, 0.5)
return Theme.outlineStrong
}
border.width: keyboardNavigationActive
&& index === selectedIndex ? 1.5 : 1
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingL
Rectangle {
width: 24
height: 24
radius: 12
color: Theme.primarySelected
anchors.verticalCenter: parent.verticalCenter
StyledText {
anchors.centerIn: parent
text: entryIndex.toString()
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.primary
}
}
Row {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 68
spacing: Theme.spacingM
Item {
width: entryType === "image" ? 48 : Theme.iconSize
height: entryType === "image" ? 48 : Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
CachingImage {
id: thumbnailImageSource
property string entryId: model.entry.split(
'\t')[0]
anchors.fill: parent
source: entryType === "image"
&& imageLoader.imageData ? `data:image/png;base64,${imageLoader.imageData}` : ""
fillMode: Image.PreserveAspectCrop
smooth: true
cache: true
visible: false
asynchronous: true
Process {
id: imageLoader
property string imageData: ""
command: ["sh", "-c", `cliphist decode ${thumbnailImageSource.entryId} | base64 -w 0`]
running: entryType === "image"
stdout: StdioCollector {
onStreamFinished: {
imageLoader.imageData = text.trim()
}
}
}
}
MultiEffect {
anchors.fill: parent
anchors.margins: 2
source: thumbnailImageSource
maskEnabled: true
maskSource: clipboardCircularMask
visible: entryType === "image"
&& thumbnailImageSource.status === Image.Ready
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: clipboardCircularMask
width: 48 - 4
height: 48 - 4
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
}
}
DankIcon {
visible: !(entryType === "image"
&& thumbnailImageSource.status
=== Image.Ready)
name: {
if (entryType === "image")
return "image"
if (entryType === "long_text")
return "subject"
return "content_copy"
}
size: Theme.iconSize
color: Theme.primary
anchors.centerIn: parent
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (entryType === "image" ? 48 : Theme.iconSize) - Theme.spacingM
spacing: Theme.spacingXS
StyledText {
text: {
switch (entryType) {
case "image":
return "Image • " + entryPreview
case "long_text":
return "Long Text"
default:
return "Text"
}
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
}
StyledText {
id: contentText
text: entryPreview
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
maximumLineCount: entryType === "long_text" ? 3 : 1
elide: Text.ElideRight
}
}
}
}
DankActionButton {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
iconName: "close"
iconSize: Theme.iconSize - 6
iconColor: Theme.error
hoverColor: Theme.errorHover
onClicked: {
deleteEntry(model.entry)
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.rightMargin: 40
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: copyEntry(model.entry)
}
}
}
}
Item {
width: parent.width
height: showKeyboardHints ? 80 + Theme.spacingL : 0
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
height: 80
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r,
Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.95)
border.color: Theme.primary
border.width: 2
opacity: showKeyboardHints ? 1 : 0
z: 100
Column {
anchors.centerIn: parent
spacing: 2
StyledText {
text: "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "Shift+Del: Clear All • Esc: Close"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
-247
View File
@@ -1,247 +0,0 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import qs.Common
PanelWindow {
id: root
property alias content: contentLoader.sourceComponent
property alias contentLoader: contentLoader
property real width: 400
property real height: 300
readonly property real screenWidth: screen ? screen.width : 1920
readonly property real screenHeight: screen ? screen.height : 1080
property bool showBackground: true
property real backgroundOpacity: 0.5
property string positioning: "center"
property point customPosition: Qt.point(0, 0)
property bool closeOnEscapeKey: true
property bool closeOnBackgroundClick: true
property string animationType: "scale"
property int animationDuration: Theme.mediumDuration
property var animationEasing: Theme.emphasizedEasing
property color backgroundColor: Theme.surfaceContainer
property color borderColor: Theme.outlineMedium
property real borderWidth: 1
property real cornerRadius: Theme.cornerRadius
property bool enableShadow: false
// Expose the focusScope for external access
property alias modalFocusScope: focusScope
property bool shouldBeVisible: false
property bool shouldHaveFocus: shouldBeVisible
property bool allowFocusOverride: false
property bool allowStacking: false
signal opened
signal dialogClosed
signal backgroundClicked
Connections {
target: ModalManager
function onCloseAllModalsExcept(excludedModal) {
if (excludedModal !== root && !allowStacking && shouldBeVisible) {
close()
}
}
}
function open() {
ModalManager.openModal(root)
closeTimer.stop()
shouldBeVisible = true
visible = true
focusScope.forceActiveFocus()
}
function close() {
shouldBeVisible = false
closeTimer.restart()
}
function toggle() {
if (shouldBeVisible)
close()
else
open()
}
visible: shouldBeVisible
color: "transparent"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: shouldHaveFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
onVisibleChanged: {
if (root.visible) {
opened()
} else {
if (Qt.inputMethod) {
Qt.inputMethod.hide()
Qt.inputMethod.reset()
}
dialogClosed()
}
}
Timer {
id: closeTimer
interval: animationDuration + 50
onTriggered: {
visible = false
}
}
anchors {
top: true
left: true
right: true
bottom: true
}
Rectangle {
id: background
anchors.fill: parent
color: "black"
opacity: root.showBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.showBackground
MouseArea {
anchors.fill: parent
enabled: root.closeOnBackgroundClick
onClicked: mouse => {
var localPos = mapToItem(contentContainer,
mouse.x, mouse.y)
if (localPos.x < 0
|| localPos.x > contentContainer.width
|| localPos.y < 0
|| localPos.y > contentContainer.height)
root.backgroundClicked()
}
}
Behavior on opacity {
NumberAnimation {
duration: root.animationDuration
easing.type: root.animationEasing
}
}
}
Rectangle {
id: contentContainer
width: root.width
height: root.height
anchors.centerIn: positioning === "center" ? parent : undefined
x: {
if (positioning === "top-right")
return Math.max(Theme.spacingL,
root.screenWidth - width - Theme.spacingL)
else if (positioning === "custom")
return root.customPosition.x
return 0 // Will be overridden by anchors.centerIn when positioning === "center"
}
y: {
if (positioning === "top-right")
return Theme.barHeight + Theme.spacingXS
else if (positioning === "custom")
return root.customPosition.y
return 0 // Will be overridden by anchors.centerIn when positioning === "center"
}
color: root.backgroundColor
radius: root.cornerRadius
border.color: root.borderColor
border.width: root.borderWidth
layer.enabled: root.enableShadow
opacity: root.shouldBeVisible ? 1 : 0
scale: {
if (root.animationType === "scale")
return root.shouldBeVisible ? 1 : 0.9
return 1
}
transform: root.animationType === "slide" ? slideTransform : null
Translate {
id: slideTransform
x: root.shouldBeVisible ? 0 : 15
y: root.shouldBeVisible ? 0 : -30
}
Loader {
id: contentLoader
anchors.fill: parent
active: root.visible
asynchronous: false
}
Behavior on opacity {
NumberAnimation {
duration: root.animationDuration
easing.type: root.animationEasing
}
}
Behavior on scale {
enabled: root.animationType === "scale"
NumberAnimation {
duration: root.animationDuration
easing.type: root.animationEasing
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1
shadowColor: Theme.shadowStrong
shadowOpacity: 0.3
}
}
FocusScope {
id: focusScope
objectName: "modalFocusScope"
anchors.fill: parent
visible: root.visible // Only active when the modal is visible
focus: root.visible
Keys.onEscapePressed: event => {
console.log(
"DankModal escape pressed - shouldHaveFocus:",
shouldHaveFocus, "closeOnEscapeKey:",
root.closeOnEscapeKey, "objectName:",
root.objectName || "unnamed")
if (root.closeOnEscapeKey
&& shouldHaveFocus) {
console.log("DankModal handling escape")
root.close()
event.accepted = true
}
}
onVisibleChanged: {
if (visible && shouldHaveFocus)
Qt.callLater(function () {
focusScope.forceActiveFocus()
})
}
Connections {
target: root
function onShouldHaveFocusChanged() {
if (shouldHaveFocus && visible) {
Qt.callLater(function () {
focusScope.forceActiveFocus()
})
}
}
}
}
}
-298
View File
@@ -1,298 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtCore
import Qt.labs.folderlistmodel
import Quickshell.Io
import qs.Common
import qs.Widgets
DankModal {
id: fileBrowserModal
objectName: "fileBrowserModal"
allowStacking: true
signal fileSelected(string path)
property string homeDir: StandardPaths.writableLocation(
StandardPaths.HomeLocation)
property string currentPath: ""
property var fileExtensions: ["*.*"]
property alias filterExtensions: fileBrowserModal.fileExtensions
property string browserTitle: "Select File"
property string browserIcon: "folder_open"
property string browserType: "generic" // "wallpaper" or "profile" for last path memory
property bool showHiddenFiles: false
FolderListModel {
id: folderModel
showDirsFirst: true
showDotAndDotDot: false
showHidden: fileBrowserModal.showHiddenFiles
nameFilters: fileExtensions
showFiles: true
showDirs: true
folder: currentPath ? "file://" + currentPath : "file://" + homeDir
}
function isImageFile(fileName) {
if (!fileName)
return false
var ext = fileName.toLowerCase().split('.').pop()
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)
}
function getLastPath() {
var lastPath = ""
if (browserType === "wallpaper") {
lastPath = SessionData.wallpaperLastPath
} else if (browserType === "profile") {
lastPath = SessionData.profileLastPath
}
if (lastPath && lastPath !== "") {
return lastPath
}
return homeDir
}
function saveLastPath(path) {
if (browserType === "wallpaper") {
SessionData.setWallpaperLastPath(path)
} else if (browserType === "profile") {
SessionData.setProfileLastPath(path)
}
}
Component.onCompleted: {
currentPath = getLastPath()
}
width: 800
height: 600
enableShadow: true
visible: false
onBackgroundClicked: close()
onVisibleChanged: {
if (visible) {
var startPath = getLastPath()
currentPath = startPath
}
}
onCurrentPathChanged: {
}
function navigateUp() {
var path = currentPath
if (path === homeDir) {
return
}
var lastSlash = path.lastIndexOf('/')
if (lastSlash > 0) {
var newPath = path.substring(0, lastSlash)
if (newPath.length < homeDir.length) {
currentPath = homeDir
saveLastPath(homeDir)
} else {
currentPath = newPath
saveLastPath(newPath)
}
}
}
function navigateTo(path) {
currentPath = path
saveLastPath(path) // Save the path when navigating
}
content: Component {
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Item {
width: parent.width
height: 40
Row {
spacing: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: browserIcon
size: Theme.iconSizeLarge
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: browserTitle
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: fileBrowserModal.close()
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
width: parent.width
spacing: Theme.spacingS
StyledRect {
width: 32
height: 32
radius: Theme.cornerRadius
color: mouseArea.containsMouse
&& currentPath !== homeDir ? Theme.surfaceVariant : "transparent"
opacity: currentPath !== homeDir ? 1.0 : 0.0
DankIcon {
anchors.centerIn: parent
name: "arrow_back"
size: Theme.iconSizeSmall
color: Theme.surfaceText
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: currentPath !== homeDir
cursorShape: currentPath
!== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: currentPath !== homeDir
onClicked: navigateUp()
}
}
StyledText {
text: fileBrowserModal.currentPath.replace("file://", "")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width - 40 - Theme.spacingS
elide: Text.ElideMiddle
anchors.verticalCenter: parent.verticalCenter
maximumLineCount: 1
wrapMode: Text.NoWrap
}
}
DankGridView {
id: fileGrid
width: parent.width
height: parent.height - 80
clip: true
cellWidth: 150
cellHeight: 130
cacheBuffer: 260
model: folderModel
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
delegate: StyledRect {
id: delegateRoot
required property bool fileIsDir
required property string filePath
required property string fileName
required property url fileURL
width: 140
height: 120
radius: Theme.cornerRadius
color: mouseArea.containsMouse ? Theme.surfaceVariant : "transparent"
border.color: Theme.outline
border.width: mouseArea.containsMouse ? 1 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Item {
width: 80
height: 60
anchors.horizontalCenter: parent.horizontalCenter
CachingImage {
anchors.fill: parent
source: (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? ("file://" + delegateRoot.filePath) : ""
fillMode: Image.PreserveAspectCrop
visible: !delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)
maxCacheSize: 80
}
DankIcon {
anchors.centerIn: parent
name: "description"
size: Theme.iconSizeLarge
color: Theme.primary
visible: !delegateRoot.fileIsDir
&& !isImageFile(delegateRoot.fileName)
}
DankIcon {
anchors.centerIn: parent
name: "folder"
size: Theme.iconSizeLarge
color: Theme.primary
visible: delegateRoot.fileIsDir
}
}
StyledText {
text: delegateRoot.fileName || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: 120
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
maximumLineCount: 2
wrapMode: Text.WordWrap
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (delegateRoot.fileIsDir) {
navigateTo(delegateRoot.filePath)
} else {
fileSelected(delegateRoot.filePath)
}
}
}
}
}
}
}
}
-209
View File
@@ -1,209 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
DankModal {
id: root
property bool networkInfoModalVisible: false
property string networkSSID: ""
property var networkData: null
property string networkDetails: ""
function showNetworkInfo(ssid, data) {
networkSSID = ssid
networkData = data
networkInfoModalVisible = true
open()
NetworkService.fetchNetworkInfo(ssid)
}
function hideDialog() {
networkInfoModalVisible = false
close()
networkSSID = ""
networkData = null
networkDetails = ""
}
visible: networkInfoModalVisible
width: 600
height: 500
enableShadow: true
onBackgroundClicked: {
hideDialog()
}
onVisibleChanged: {
if (!visible) {
networkSSID = ""
networkData = null
networkDetails = ""
}
}
content: Component {
Item {
anchors.fill: parent
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Row {
width: parent.width
Column {
width: parent.width - 40
spacing: Theme.spacingXS
StyledText {
text: "Network Information"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: "Details for \"" + networkSSID + "\""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
elide: Text.ElideRight
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: {
root.hideDialog()
}
}
}
Flickable {
width: parent.width
height: parent.height - 140
clip: true
contentWidth: width
contentHeight: detailsRect.height
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
interactive: true
flickDeceleration: 1500
maximumFlickVelocity: 2000
boundsBehavior: Flickable.DragAndOvershootBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => {
let delta = event.pixelDelta.y
!== 0 ? event.pixelDelta.y
* 1.8 : event.angleDelta.y / 120 * 60
let newY = parent.contentY - delta
newY = Math.max(
0, Math.min(
parent.contentHeight - parent.height,
newY))
parent.contentY = newY
event.accepted = true
}
}
Rectangle {
id: detailsRect
width: parent.width
height: Math.max(
parent.parent.height,
detailsText.contentHeight + Theme.spacingM * 2)
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: Theme.outlineStrong
border.width: 1
StyledText {
id: detailsText
anchors.fill: parent
anchors.margins: Theme.spacingM
text: NetworkService.networkInfoDetails.replace(
/\\n/g,
'\n') || "No information available"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
wrapMode: Text.WordWrap
lineHeight: 1.5
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
}
Item {
width: parent.width
height: 40
Rectangle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: Math.max(
70,
closeText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: closeArea.containsMouse ? Qt.darker(
Theme.primary,
1.1) : Theme.primary
StyledText {
id: closeText
anchors.centerIn: parent
text: "Close"
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hideDialog()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}
-135
View File
@@ -1,135 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Modules.Notifications.Center
import qs.Services
import qs.Widgets
DankModal {
id: notificationModal
width: 500
height: 700
visible: false
onBackgroundClicked: hide()
onDialogClosed: {
notificationModalOpen = false
modalKeyboardController.reset()
}
modalFocusScope.Keys.onPressed: function (event) {
modalKeyboardController.handleKey(event)
}
NotificationKeyboardController {
id: modalKeyboardController
listView: null
isOpen: notificationModal.notificationModalOpen
onClose: function () {
notificationModal.hide()
}
}
property bool notificationModalOpen: false
property var notificationListRef: null
function show() {
notificationModalOpen = true
NotificationService.onOverlayOpen()
open()
modalKeyboardController.reset()
if (modalKeyboardController && notificationListRef) {
modalKeyboardController.listView = notificationListRef
modalKeyboardController.rebuildFlatNavigation()
}
}
function hide() {
notificationModalOpen = false
NotificationService.onOverlayClose()
close()
modalKeyboardController.reset()
}
function toggle() {
if (shouldBeVisible)
hide()
else
show()
}
IpcHandler {
function open() {
notificationModal.show()
return "NOTIFICATION_MODAL_OPEN_SUCCESS"
}
function close() {
notificationModal.hide()
return "NOTIFICATION_MODAL_CLOSE_SUCCESS"
}
function toggle() {
notificationModal.toggle()
return "NOTIFICATION_MODAL_TOGGLE_SUCCESS"
}
target: "notifications"
}
property Component notificationContent: Component {
Item {
id: notificationKeyHandler
anchors.fill: parent
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
NotificationHeader {
id: notificationHeader
keyboardController: modalKeyboardController
}
NotificationSettings {
id: notificationSettings
expanded: notificationHeader.showSettings
}
KeyboardNavigatedNotificationList {
id: notificationList
width: parent.width
height: parent.height - y
keyboardController: modalKeyboardController
Component.onCompleted: {
notificationModal.notificationListRef = notificationList
if (modalKeyboardController) {
modalKeyboardController.listView = notificationList
modalKeyboardController.rebuildFlatNavigation()
}
}
}
}
NotificationKeyboardHints {
id: keyboardHints
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
showHints: modalKeyboardController.showKeyboardHints
}
}
}
content: notificationContent
}
-166
View File
@@ -1,166 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
DankModal {
id: root
property string powerConfirmAction: ""
property string powerConfirmTitle: ""
property string powerConfirmMessage: ""
function show(action, title, message) {
powerConfirmAction = action
powerConfirmTitle = title
powerConfirmMessage = message
open()
}
function executePowerAction(action) {
switch (action) {
case "logout":
CompositorService.logout()
break
case "suspend":
SessionService.suspend()
break
case "reboot":
SessionService.reboot()
break
case "poweroff":
SessionService.poweroff()
break
}
}
shouldBeVisible: false
width: 350
height: 160
enableShadow: false
onBackgroundClicked: {
close()
}
content: Component {
Item {
anchors.fill: parent
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingM
StyledText {
text: powerConfirmTitle
font.pixelSize: Theme.fontSizeLarge
color: {
switch (powerConfirmAction) {
case "poweroff":
return Theme.error
case "reboot":
return Theme.warning
default:
return Theme.surfaceText
}
}
font.weight: Font.Medium
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: powerConfirmMessage
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
Item {
height: Theme.spacingS
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: cancelButton.containsMouse ? Theme.surfaceTextPressed : Theme.surfaceVariantAlpha
StyledText {
text: "Cancel"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: cancelButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
close()
}
}
}
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: {
let baseColor
switch (powerConfirmAction) {
case "poweroff":
baseColor = Theme.error
break
case "reboot":
baseColor = Theme.warning
break
default:
baseColor = Theme.primary
break
}
return confirmButton.containsMouse ? Qt.rgba(
baseColor.r,
baseColor.g,
baseColor.b,
0.9) : baseColor
}
StyledText {
text: "Confirm"
font.pixelSize: Theme.fontSizeMedium
color: Theme.primaryText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: confirmButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
close()
executePowerAction(powerConfirmAction)
}
}
}
}
}
}
}
}
-335
View File
@@ -1,335 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules.ProcessList
import qs.Services
import qs.Widgets
DankModal {
id: processListModal
property int currentTab: 0
property var tabNames: ["Processes", "Performance", "System"]
function show() {
if (!DgopService.dgopAvailable) {
console.warn("ProcessListModal: dgop is not available")
return
}
open()
UserInfoService.getUptime()
}
function hide() {
close()
if (processContextMenu.visible)
processContextMenu.close()
}
function toggle() {
if (!DgopService.dgopAvailable) {
console.warn("ProcessListModal: dgop is not available")
return
}
if (shouldBeVisible)
hide()
else
show()
}
width: 900
height: 680
visible: false
backgroundColor: Theme.popupBackground()
cornerRadius: Theme.cornerRadius
enableShadow: true
onBackgroundClicked: hide()
Component {
id: processesTabComponent
ProcessesTab {
contextMenu: processContextMenu
}
}
Component {
id: performanceTabComponent
PerformanceTab {}
}
Component {
id: systemTabComponent
SystemTab {}
}
ProcessContextMenu {
id: processContextMenu
}
content: Component {
Item {
anchors.fill: parent
focus: true
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
processListModal.hide()
event.accepted = true
} else if (event.key === Qt.Key_1) {
currentTab = 0
event.accepted = true
} else if (event.key === Qt.Key_2) {
currentTab = 1
event.accepted = true
} else if (event.key === Qt.Key_3) {
currentTab = 2
event.accepted = true
}
}
// Show error message when dgop is not available
Rectangle {
anchors.centerIn: parent
width: 400
height: 200
radius: Theme.cornerRadius
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.1)
border.color: Theme.error
border.width: 2
visible: !DgopService.dgopAvailable
Column {
anchors.centerIn: parent
spacing: Theme.spacingL
DankIcon {
name: "error"
size: 48
color: Theme.error
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "System Monitor Unavailable"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.error
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature."
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
visible: DgopService.dgopAvailable
RowLayout {
Layout.fillWidth: true
height: 40
StyledText {
text: "System Monitor"
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: processListModal.hide()
Layout.alignment: Qt.AlignVCenter
}
}
Rectangle {
Layout.fillWidth: true
height: 52
color: Theme.surfaceSelected
radius: Theme.cornerRadius
border.color: Theme.outlineLight
border.width: 1
Row {
anchors.fill: parent
anchors.margins: 4
spacing: 2
Repeater {
model: tabNames
Rectangle {
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
height: 44
radius: Theme.cornerRadius
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
border.color: currentTab === index ? Theme.primary : "transparent"
border.width: currentTab === index ? 1 : 0
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: {
switch (index) {
case 0:
return "list_alt"
case 1:
return "analytics"
case 2:
return "settings"
default:
return "tab"
}
}
size: Theme.iconSize - 2
color: currentTab
=== index ? Theme.primary : Theme.surfaceText
opacity: currentTab === index ? 1 : 0.7
anchors.verticalCenter: parent.verticalCenter
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: currentTab
=== index ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -1
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
currentTab = index
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.surfaceLight
border.color: Theme.outlineLight
border.width: 1
Loader {
id: processesTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 0
visible: currentTab === 0
opacity: currentTab === 0 ? 1 : 0
sourceComponent: processesTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Loader {
id: performanceTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 1
visible: currentTab === 1
opacity: currentTab === 1 ? 1 : 0
sourceComponent: performanceTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Loader {
id: systemTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 2
visible: currentTab === 2
opacity: currentTab === 2 ? 1 : 0
sourceComponent: systemTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}
}
}
-568
View File
@@ -1,568 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Modules.Settings
import qs.Services
import qs.Widgets
DankModal {
id: settingsModal
property Component settingsContent
signal closingModal
function show() {
open()
}
function hide() {
close()
}
function toggle() {
if (shouldBeVisible)
hide()
else
show()
}
objectName: "settingsModal"
width: 800
height: 750
visible: false
onBackgroundClicked: hide()
content: settingsContent
IpcHandler {
function open() {
settingsModal.show()
return "SETTINGS_OPEN_SUCCESS"
}
function close() {
settingsModal.hide()
return "SETTINGS_CLOSE_SUCCESS"
}
function toggle() {
settingsModal.toggle()
return "SETTINGS_TOGGLE_SUCCESS"
}
target: "settings"
}
settingsContent: Component {
Item {
anchors.fill: parent
focus: true
Column {
anchors.fill: parent
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.topMargin: Theme.spacingM
anchors.bottomMargin: Theme.spacingL
spacing: 0
Item {
width: parent.width
height: 35
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "settings"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Settings"
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
DankActionButton {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: settingsModal.hide()
}
}
Row {
width: parent.width
height: parent.height - 35
spacing: 0
Rectangle {
id: sidebarContainer
property int currentIndex: 0
width: 270
height: parent.height
color: Theme.surfaceContainer
radius: Theme.cornerRadius
Column {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
anchors.topMargin: Theme.spacingM + 2
spacing: Theme.spacingXS
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 110
radius: Theme.cornerRadius
color: "transparent"
border.width: 0
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
Item {
id: profileImageContainer
width: 80
height: 80
anchors.verticalCenter: parent.verticalCenter
property bool hasImage: profileImageSource.status
=== Image.Ready
Rectangle {
anchors.fill: parent
radius: width / 2
color: "transparent"
border.color: Theme.primary
border.width: 1
visible: parent.hasImage
}
Image {
id: profileImageSource
source: {
if (PortalService.profileImage === "")
return ""
if (PortalService.profileImage.startsWith(
"/"))
return "file://" + PortalService.profileImage
return PortalService.profileImage
}
smooth: true
asynchronous: true
mipmap: true
cache: true
visible: false
}
MultiEffect {
anchors.fill: parent
anchors.margins: 5
source: profileImageSource
maskEnabled: true
maskSource: profileCircularMask
visible: profileImageContainer.hasImage
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: profileCircularMask
width: 70
height: 70
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
}
}
Rectangle {
anchors.fill: parent
radius: width / 2
color: Theme.primary
visible: !parent.hasImage
DankIcon {
anchors.centerIn: parent
name: "person"
size: Theme.iconSizeLarge
color: Theme.primaryText
}
}
DankIcon {
anchors.centerIn: parent
name: "warning"
size: Theme.iconSizeLarge
color: Theme.error
visible: PortalService.profileImage !== ""
&& profileImageSource.status === Image.Error
}
Rectangle {
anchors.fill: parent
radius: width / 2
color: Qt.rgba(0, 0, 0, 0.7)
visible: profileMouseArea.containsMouse
Row {
anchors.centerIn: parent
spacing: 4
Rectangle {
width: 28
height: 28
radius: 14
color: Qt.rgba(255, 255,
255, 0.9)
DankIcon {
anchors.centerIn: parent
name: "edit"
size: 16
color: "black"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
settingsModal.allowFocusOverride = true
settingsModal.shouldHaveFocus = false
profileBrowser.open(
)
}
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: Qt.rgba(255, 255,
255, 0.9)
visible: profileImageContainer.hasImage
DankIcon {
anchors.centerIn: parent
name: "close"
size: 16
color: "black"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
PortalService.setProfileImage(
"")
}
}
}
}
}
MouseArea {
id: profileMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
propagateComposedEvents: true
acceptedButtons: Qt.NoButton
}
}
Column {
width: 120
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: UserInfoService.fullName
|| "User"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
width: parent.width
}
StyledText {
text: DgopService.distribution
|| "Linux"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
}
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 1
color: Theme.outline
opacity: 0.2
}
Item {
width: parent.width
height: Theme.spacingL
}
Repeater {
id: sidebarRepeater
model: [{
"text": "Personalization",
"icon": "person"
}, {
"text": "Time & Date",
"icon": "schedule"
}, {
"text": "Weather",
"icon": "cloud"
}, {
"text": "Top Bar",
"icon": "toolbar"
}, {
"text": "Widgets",
"icon": "widgets"
}, {
"text": "Dock",
"icon": "dock_to_bottom"
}, {
"text": "Recent Apps",
"icon": "history"
}, {
"text": "Theme & Colors",
"icon": "palette"
}, {
"text": "About",
"icon": "info"
}]
Rectangle {
property bool isActive: sidebarContainer.currentIndex === index
width: parent.width - Theme.spacingS * 2
height: 44
radius: Theme.cornerRadius
color: isActive ? Theme.surfaceContainerHigh : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: modelData.icon || ""
size: Theme.iconSize - 2
color: parent.parent.isActive ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.text || ""
font.pixelSize: Theme.fontSizeMedium
color: parent.parent.isActive ? Theme.primary : Theme.surfaceText
font.weight: parent.parent.isActive ? Font.Medium : Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
sidebarContainer.currentIndex = index
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
Item {
width: parent.width - sidebarContainer.width
height: parent.height
Rectangle {
anchors.fill: parent
anchors.leftMargin: 0
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingM
anchors.topMargin: 0
color: "transparent"
Loader {
id: personalizationLoader
anchors.fill: parent
active: sidebarContainer.currentIndex === 0
visible: active
asynchronous: true
sourceComponent: Component {
PersonalizationTab {
parentModal: settingsModal
}
}
}
Loader {
id: timeLoader
anchors.fill: parent
active: sidebarContainer.currentIndex === 1
visible: active
asynchronous: true
sourceComponent: TimeTab {}
}
Loader {
id: weatherLoader
anchors.fill: parent
active: sidebarContainer.currentIndex === 2
visible: active
asynchronous: true
sourceComponent: WeatherTab {}
}
Loader {
id: topBarLoader
anchors.fill: parent
active: sidebarContainer.currentIndex === 3
visible: active
asynchronous: true
sourceComponent: TopBarTab {}
}
Loader {
id: widgetsLoader
anchors.fill: parent
active: sidebarContainer.currentIndex === 4
visible: active
asynchronous: true
sourceComponent: WidgetTweaksTab {}
}
Loader {
id: dockLoader
anchors.fill: parent
active: sidebarContainer.currentIndex === 5
visible: active
asynchronous: true
sourceComponent: Component {
DockTab {}
}
}
Loader {
id: recentAppsLoader
anchors.fill: parent
active: sidebarContainer.currentIndex === 6
visible: active
asynchronous: true
sourceComponent: RecentAppsTab {}
}
Loader {
id: themeColorsLoader
anchors.fill: parent
active: sidebarContainer.currentIndex === 7
visible: active
asynchronous: true
sourceComponent: ThemeColorsTab {}
}
Loader {
id: aboutLoader
anchors.fill: parent
active: sidebarContainer.currentIndex === 8
visible: active
asynchronous: true
sourceComponent: AboutTab {}
}
}
}
}
}
}
}
FileBrowserModal {
id: profileBrowser
browserTitle: "Select Profile Image"
browserIcon: "person"
browserType: "profile"
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: path => {
PortalService.setProfileImage(path)
close()
}
onDialogClosed: {
if (settingsModal) {
settingsModal.allowFocusOverride = false
settingsModal.shouldHaveFocus = Qt.binding(() => {
return settingsModal.shouldBeVisible
})
}
}
}
}
-919
View File
@@ -1,919 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Modules.AppDrawer
import qs.Services
import qs.Widgets
DankModal {
id: spotlightModal
property bool spotlightOpen: false
property Component spotlightContent
function show() {
spotlightOpen = true
open()
if (contentLoader.item && contentLoader.item.appLauncher)
contentLoader.item.appLauncher.searchQuery = ""
Qt.callLater(function () {
if (contentLoader.item && contentLoader.item.searchField)
contentLoader.item.searchField.forceActiveFocus()
})
}
function hide() {
spotlightOpen = false
close()
if (contentLoader.item && contentLoader.item.appLauncher) {
contentLoader.item.appLauncher.searchQuery = ""
contentLoader.item.appLauncher.selectedIndex = 0
contentLoader.item.appLauncher.setCategory("All")
}
}
function toggle() {
if (spotlightOpen)
hide()
else
show()
}
shouldBeVisible: spotlightOpen
Connections {
target: ModalManager
function onCloseAllModalsExcept(excludedModal) {
if (excludedModal !== spotlightModal && !allowStacking
&& spotlightOpen) {
spotlightOpen = false
}
}
}
width: 550
height: 600
backgroundColor: Theme.popupBackground()
cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium
borderWidth: 1
enableShadow: true
onVisibleChanged: {
if (visible && !spotlightOpen)
show()
if (visible && contentLoader.item)
Qt.callLater(function () {
if (contentLoader.item.searchField)
contentLoader.item.searchField.forceActiveFocus()
})
}
onBackgroundClicked: {
hide()
}
Component.onCompleted: {
}
content: spotlightContent
IpcHandler {
function open() {
spotlightModal.show()
return "SPOTLIGHT_OPEN_SUCCESS"
}
function close() {
spotlightModal.hide()
return "SPOTLIGHT_CLOSE_SUCCESS"
}
function toggle() {
spotlightModal.toggle()
return "SPOTLIGHT_TOGGLE_SUCCESS"
}
target: "spotlight"
}
spotlightContent: Component {
Item {
id: spotlightKeyHandler
property alias appLauncher: appLauncher
property alias searchField: searchField
anchors.fill: parent
focus: true
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
hide()
event.accepted = true
} else if (event.key === Qt.Key_Down) {
appLauncher.selectNext()
event.accepted = true
} else if (event.key === Qt.Key_Up) {
appLauncher.selectPrevious()
event.accepted = true
} else if (event.key === Qt.Key_Right
&& appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow()
event.accepted = true
} else if (event.key === Qt.Key_Left
&& appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow()
event.accepted = true
} else if (event.key === Qt.Key_Return
|| event.key === Qt.Key_Enter) {
appLauncher.launchSelected()
event.accepted = true
} else if (!searchField.activeFocus && event.text
&& event.text.length > 0 && event.text.match(
/[a-zA-Z0-9\\s]/)) {
searchField.forceActiveFocus()
searchField.insertText(event.text)
event.accepted = true
}
}
AppLauncher {
id: appLauncher
viewMode: SettingsData.spotlightModalViewMode
gridColumns: 4
onAppLaunched: hide()
onViewModeSelected: function (mode) {
SettingsData.setSpotlightModalViewMode(mode)
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Rectangle {
width: parent.width
height: categorySelector.height + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceVariantAlpha
border.color: Theme.outlineMedium
border.width: 1
visible: appLauncher.categories.length > 1
|| appLauncher.model.count > 0
CategorySelector {
id: categorySelector
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
categories: appLauncher.categories
selectedCategory: appLauncher.selectedCategory
compact: false
onCategorySelected: category => {
return appLauncher.setCategory(
category)
}
}
}
Row {
width: parent.width
spacing: Theme.spacingM
DankTextField {
id: searchField
width: parent.width - 80
- Theme.spacingM // Leave space for view toggle buttons
height: 56
cornerRadius: Theme.cornerRadius
backgroundColor: Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
Theme.getContentBackgroundAlpha(
) * 0.7)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
enabled: spotlightOpen
placeholderText: ""
ignoreLeftRightKeys: true
keyForwardTargets: [spotlightKeyHandler]
text: appLauncher.searchQuery
onTextEdited: {
appLauncher.searchQuery = text
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
hide()
event.accepted = true
} else if ((event.key === Qt.Key_Return
|| event.key === Qt.Key_Enter)
&& text.length > 0) {
if (appLauncher.keyboardNavigationActive
&& appLauncher.model.count > 0)
appLauncher.launchSelected()
else if (appLauncher.model.count > 0)
appLauncher.launchApp(
appLauncher.model.get(0))
event.accepted = true
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
event.accepted = false
}
}
}
Row {
spacing: Theme.spacingXS
visible: appLauncher.model.count > 0
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: 36
height: 36
radius: Theme.cornerRadius
color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent"
border.color: appLauncher.viewMode
=== "list" ? Theme.primarySelected : "transparent"
border.width: 1
DankIcon {
anchors.centerIn: parent
name: "view_list"
size: 18
color: appLauncher.viewMode
=== "list" ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: listViewArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
appLauncher.setViewMode("list")
}
}
}
Rectangle {
width: 36
height: 36
radius: Theme.cornerRadius
color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent"
border.color: appLauncher.viewMode
=== "grid" ? Theme.primarySelected : "transparent"
border.width: 1
DankIcon {
anchors.centerIn: parent
name: "grid_view"
size: 18
color: appLauncher.viewMode
=== "grid" ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: gridViewArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
appLauncher.setViewMode("grid")
}
}
}
}
}
Rectangle {
id: resultsContainer
width: parent.width
height: parent.height - y
radius: Theme.cornerRadius
color: Theme.surfaceLight
border.color: Theme.outlineLight
border.width: 1
DankListView {
id: resultsList
property int itemHeight: 60
property int iconSize: 40
property bool showDescription: true
property int itemSpacing: Theme.spacingS
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
signal keyboardNavigationReset
signal itemClicked(int index, var modelData)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= count)
return
var itemY = index * (itemHeight + itemSpacing)
var itemBottom = itemY + itemHeight
if (itemY < contentY)
contentY = itemY
else if (itemBottom > contentY + height)
contentY = itemBottom - height
}
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: appLauncher.viewMode === "list"
model: appLauncher.model
currentIndex: appLauncher.selectedIndex
clip: true
spacing: itemSpacing
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex)
}
onItemClicked: function (index, modelData) {
appLauncher.launchApp(modelData)
}
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
contextMenu.show(mouseX, mouseY, modelData)
}
onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AlwaysOn
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
delegate: Rectangle {
width: ListView.view.width
height: resultsList.itemHeight
radius: Theme.cornerRadius
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium
border.width: ListView.isCurrentItem ? 2 : 1
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingL
Item {
width: resultsList.iconSize
height: resultsList.iconSize
anchors.verticalCenter: parent.verticalCenter
IconImage {
id: listIconImg
anchors.fill: parent
source: (model.icon) ? Quickshell.iconPath(
model.icon,
SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
}
Rectangle {
anchors.fill: parent
visible: !listIconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.primarySelected
StyledText {
anchors.centerIn: parent
text: (model.name
&& model.name.length
> 0) ? model.name.charAt(
0).toUpperCase(
) : "A"
font.pixelSize: resultsList.iconSize * 0.4
color: Theme.primary
font.weight: Font.Bold
}
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - resultsList.iconSize - Theme.spacingL
spacing: Theme.spacingXS
StyledText {
width: parent.width
text: model.name || ""
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: model.comment || "Application"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
elide: Text.ElideRight
visible: resultsList.showDescription
&& model.comment
&& model.comment.length > 0
}
}
}
MouseArea {
id: listMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
z: 10
onEntered: {
if (resultsList.hoverUpdatesSelection
&& !resultsList.keyboardNavigationActive)
resultsList.currentIndex = index
}
onPositionChanged: {
resultsList.keyboardNavigationReset()
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
resultsList.itemClicked(
index, model)
} else if (mouse.button === Qt.RightButton) {
var modalPos = mapToItem(
spotlightKeyHandler,
mouse.x, mouse.y)
resultsList.itemRightClicked(
index, model,
modalPos.x, modalPos.y)
}
}
}
}
}
DankGridView {
id: resultsGrid
property int currentIndex: appLauncher.selectedIndex
property int columns: 4
property bool adaptiveColumns: false
property int minCellWidth: 120
property int maxCellWidth: 160
property int cellPadding: 8
property real iconSizeRatio: 0.55
property int maxIconSize: 48
property int minIconSize: 32
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
property int baseCellWidth: adaptiveColumns ? Math.max(
minCellWidth,
Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
property int baseCellHeight: baseCellWidth + 20
property int actualColumns: adaptiveColumns ? Math.floor(
width
/ cellWidth) : columns
property int remainingSpace: width - (actualColumns * cellWidth)
signal keyboardNavigationReset
signal itemClicked(int index, var modelData)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= count)
return
var itemY = Math.floor(
index / actualColumns) * cellHeight
var itemBottom = itemY + cellHeight
if (itemY < contentY)
contentY = itemY
else if (itemBottom > contentY + height)
contentY = itemBottom - height
}
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: appLauncher.viewMode === "grid"
model: appLauncher.model
clip: true
cellWidth: baseCellWidth
cellHeight: baseCellHeight
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
rightMargin: leftMargin
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex)
}
onItemClicked: function (index, modelData) {
appLauncher.launchApp(modelData)
}
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
contextMenu.show(mouseX, mouseY, modelData)
}
onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
delegate: Rectangle {
width: resultsGrid.cellWidth - resultsGrid.cellPadding
height: resultsGrid.cellHeight - resultsGrid.cellPadding
radius: Theme.cornerRadius
color: resultsGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
border.color: resultsGrid.currentIndex
=== index ? Theme.primarySelected : Theme.outlineMedium
border.width: resultsGrid.currentIndex === index ? 2 : 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
Item {
property int iconSize: Math.min(
resultsGrid.maxIconSize,
Math.max(
resultsGrid.minIconSize,
resultsGrid.cellWidth
* resultsGrid.iconSizeRatio))
width: iconSize
height: iconSize
anchors.horizontalCenter: parent.horizontalCenter
IconImage {
id: gridIconImg
anchors.fill: parent
source: (model.icon) ? Quickshell.iconPath(
model.icon,
SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
}
Rectangle {
anchors.fill: parent
visible: !gridIconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.primarySelected
StyledText {
anchors.centerIn: parent
text: (model.name
&& model.name.length
> 0) ? model.name.charAt(
0).toUpperCase(
) : "A"
font.pixelSize: Math.min(
28,
parent.width * 0.5)
color: Theme.primary
font.weight: Font.Bold
}
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
width: resultsGrid.cellWidth - 12
text: model.name || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
maximumLineCount: 2
wrapMode: Text.WordWrap
}
}
MouseArea {
id: gridMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
z: 10
onEntered: {
if (resultsGrid.hoverUpdatesSelection
&& !resultsGrid.keyboardNavigationActive)
resultsGrid.currentIndex = index
}
onPositionChanged: {
resultsGrid.keyboardNavigationReset()
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
resultsGrid.itemClicked(
index, model)
} else if (mouse.button === Qt.RightButton) {
var modalPos = mapToItem(
spotlightKeyHandler,
mouse.x, mouse.y)
resultsGrid.itemRightClicked(
index, model,
modalPos.x, modalPos.y)
}
}
}
}
}
}
}
Rectangle {
id: contextMenu
property var currentApp: null
property bool menuVisible: false
function show(x, y, app) {
currentApp = app
const menuWidth = 180
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
let finalX = x + 8
let finalY = y + 8
if (finalX + menuWidth > spotlightKeyHandler.width) {
finalX = x - menuWidth - 8
}
if (finalY + menuHeight > spotlightKeyHandler.height) {
finalY = y - menuHeight - 8
}
finalX = Math.max(
8, Math.min(
finalX,
spotlightKeyHandler.width - menuWidth - 8))
finalY = Math.max(
8, Math.min(
finalY,
spotlightKeyHandler.height - menuHeight - 8))
contextMenu.x = finalX
contextMenu.y = finalY
contextMenu.visible = true
contextMenu.menuVisible = true
}
function close() {
contextMenu.menuVisible = false
Qt.callLater(() => {
contextMenu.visible = false
})
}
visible: false
width: 180
height: menuColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
z: 1000
opacity: menuVisible ? 1 : 0
scale: menuVisible ? 1 : 0.85
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: pinMouseArea.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: {
if (!contextMenu.currentApp
|| !contextMenu.currentApp.desktopEntry)
return "push_pin"
var appId = contextMenu.currentApp.desktopEntry.id
|| contextMenu.currentApp.desktopEntry.execString
|| ""
return SessionData.isPinnedApp(
appId) ? "keep_off" : "push_pin"
}
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (!contextMenu.currentApp
|| !contextMenu.currentApp.desktopEntry)
return "Pin to Dock"
var appId = contextMenu.currentApp.desktopEntry.id
|| contextMenu.currentApp.desktopEntry.execString
|| ""
return SessionData.isPinnedApp(
appId) ? "Unpin from Dock" : "Pin to Dock"
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: pinMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!contextMenu.currentApp
|| !contextMenu.currentApp.desktopEntry)
return
var appId = contextMenu.currentApp.desktopEntry.id
|| contextMenu.currentApp.desktopEntry.execString
|| ""
if (SessionData.isPinnedApp(appId))
SessionData.removePinnedApp(appId)
else
SessionData.addPinnedApp(appId)
contextMenu.close()
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: launchMouseArea.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "launch"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Launch"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: launchMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (contextMenu.currentApp)
appLauncher.launchApp(
contextMenu.currentApp)
contextMenu.close()
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
MouseArea {
anchors.fill: parent
visible: contextMenu.visible
z: 999
onClicked: {
contextMenu.close()
}
MouseArea {
x: contextMenu.x
y: contextMenu.y
width: contextMenu.width
height: contextMenu.height
onClicked: {
// Prevent closing when clicking on the menu itself
}
}
}
}
}
}
-310
View File
@@ -1,310 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
DankModal {
id: root
property string wifiPasswordSSID: ""
property string wifiPasswordInput: ""
function show(ssid) {
wifiPasswordSSID = ssid
wifiPasswordInput = ""
open()
Qt.callLater(function () {
if (contentLoader.item && contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus()
}
})
}
shouldBeVisible: false
width: 420
height: 230
onShouldBeVisibleChanged: {
if (!shouldBeVisible)
wifiPasswordInput = ""
}
onOpened: {
Qt.callLater(function () {
if (contentLoader.item && contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus()
}
})
}
onBackgroundClicked: {
close()
wifiPasswordInput = ""
}
Connections {
function onPasswordDialogShouldReopenChanged() {
if (NetworkService.passwordDialogShouldReopen
&& NetworkService.connectingSSID !== "") {
wifiPasswordSSID = NetworkService.connectingSSID
wifiPasswordInput = ""
open()
NetworkService.passwordDialogShouldReopen = false
}
}
target: NetworkService
}
content: Component {
FocusScope {
id: wifiContent
property alias passwordInput: passwordInput
anchors.fill: parent
focus: true
Keys.onEscapePressed: function (event) {
close()
wifiPasswordInput = ""
event.accepted = true
}
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingM
Row {
width: parent.width
Column {
width: parent.width - 40
spacing: Theme.spacingXS
StyledText {
text: "Connect to Wi-Fi"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: "Enter password for \"" + wifiPasswordSSID + "\""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
elide: Text.ElideRight
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: {
close()
wifiPasswordInput = ""
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: passwordInput.activeFocus ? 2 : 1
MouseArea {
anchors.fill: parent
onClicked: {
passwordInput.forceActiveFocus()
}
}
DankTextField {
id: passwordInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiPasswordInput
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
placeholderText: ""
backgroundColor: "transparent"
focus: true
enabled: root.shouldBeVisible
onTextEdited: {
wifiPasswordInput = text
}
onAccepted: {
NetworkService.connectToWifiWithPassword(
wifiPasswordSSID, passwordInput.text)
close()
wifiPasswordInput = ""
passwordInput.text = ""
}
Component.onCompleted: {
if (root.shouldBeVisible) {
focusDelayTimer.start()
}
}
Timer {
id: focusDelayTimer
interval: 100
repeat: false
onTriggered: {
if (root.shouldBeVisible) {
passwordInput.forceActiveFocus()
}
}
}
Connections {
target: root
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
focusDelayTimer.start()
}
}
}
}
}
Row {
spacing: Theme.spacingS
Rectangle {
id: showPasswordCheckbox
property bool checked: false
width: 20
height: 20
radius: 4
color: checked ? Theme.primary : "transparent"
border.color: checked ? Theme.primary : Theme.outlineButton
border.width: 2
DankIcon {
anchors.centerIn: parent
name: "check"
size: 12
color: Theme.background
visible: parent.checked
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
}
}
}
StyledText {
text: "Show password"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Item {
width: parent.width
height: 40
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Rectangle {
width: Math.max(
70,
cancelText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
border.color: Theme.surfaceVariantAlpha
border.width: 1
StyledText {
id: cancelText
anchors.centerIn: parent
text: "Cancel"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
close()
wifiPasswordInput = ""
}
}
}
Rectangle {
width: Math.max(
80,
connectText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: connectArea.containsMouse ? Qt.darker(
Theme.primary,
1.1) : Theme.primary
enabled: passwordInput.text.length > 0
opacity: enabled ? 1 : 0.5
StyledText {
id: connectText
anchors.centerIn: parent
text: "Connect"
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: connectArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: {
NetworkService.connectToWifiWithPassword(
wifiPasswordSSID,
passwordInput.text)
close()
wifiPasswordInput = ""
passwordInput.text = ""
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}
}
-949
View File
@@ -1,949 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules.AppDrawer
import qs.Services
import qs.Widgets
DankPopout {
id: appDrawerPopout
property string triggerSection: "left"
property var triggerScreen: null
function show() {
open()
appLauncher.searchQuery = ""
appLauncher.selectedIndex = 0
appLauncher.setCategory("All")
appLauncher.keyboardNavigationActive = false
}
function hide() {
close()
}
function setTriggerPosition(x, y, width, section, screen) {
triggerX = x
triggerY = y
triggerWidth = width
triggerSection = section
triggerScreen = screen
}
popupWidth: 520
popupHeight: 600
triggerX: Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
triggerWidth: 40
positioning: "center"
WlrLayershell.namespace: "quickshell-launcher"
screen: triggerScreen
onOpened: {
Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.forceActiveFocus()
}
})
}
AppLauncher {
id: appLauncher
viewMode: SettingsData.appLauncherViewMode
gridColumns: 4
onAppLaunched: appDrawerPopout.hide()
onViewModeSelected: function (mode) {
SettingsData.setAppLauncherViewMode(mode)
}
}
content: Component {
Rectangle {
id: launcherPanel
property alias searchField: searchField
color: Theme.popupBackground()
radius: Theme.cornerRadius
antialiasing: true
smooth: true
Rectangle {
anchors.fill: parent
anchors.margins: -3
color: "transparent"
radius: parent.radius + 3
border.color: Qt.rgba(0, 0, 0, 0.05)
border.width: 1
z: -3
}
Rectangle {
anchors.fill: parent
anchors.margins: -2
color: "transparent"
radius: parent.radius + 2
border.color: Qt.rgba(0, 0, 0, 0.08)
border.width: 1
z: -2
}
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.12)
border.width: 1
radius: parent.radius
z: -1
}
Item {
id: keyHandler
anchors.fill: parent
focus: true
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
appDrawerPopout.close()
event.accepted = true
} else if (event.key === Qt.Key_Down) {
appLauncher.selectNext()
event.accepted = true
} else if (event.key === Qt.Key_Up) {
appLauncher.selectPrevious()
event.accepted = true
} else if (event.key === Qt.Key_Right
&& appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow()
event.accepted = true
} else if (event.key === Qt.Key_Left
&& appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow()
event.accepted = true
} else if (event.key === Qt.Key_Return
|| event.key === Qt.Key_Enter) {
appLauncher.launchSelected()
event.accepted = true
} else if (!searchField.activeFocus && event.text
&& event.text.length > 0 && event.text.match(
/[a-zA-Z0-9\\s]/)) {
searchField.forceActiveFocus()
searchField.insertText(event.text)
event.accepted = true
}
}
Column {
width: parent.width - Theme.spacingL * 2
height: parent.height - Theme.spacingL * 2
x: Theme.spacingL
y: Theme.spacingL
spacing: Theme.spacingL
Row {
width: parent.width
height: 40
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: "Applications"
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
}
Item {
width: parent.width - 200
height: 1
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: appLauncher.model.count + " apps"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
}
DankTextField {
id: searchField
width: parent.width
height: 52
cornerRadius: Theme.cornerRadius
backgroundColor: Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
Theme.getContentBackgroundAlpha(
) * 0.7)
normalBorderColor: Qt.rgba(Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.3)
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
font.pixelSize: Theme.fontSizeLarge
enabled: appDrawerPopout.shouldBeVisible
ignoreLeftRightKeys: true
keyForwardTargets: [keyHandler]
onTextEdited: {
appLauncher.searchQuery = text
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
appDrawerPopout.close()
event.accepted = true
} else if ((event.key === Qt.Key_Return
|| event.key === Qt.Key_Enter)
&& text.length > 0) {
if (appLauncher.keyboardNavigationActive
&& appLauncher.model.count > 0) {
appLauncher.launchSelected()
} else if (appLauncher.model.count > 0) {
var firstApp = appLauncher.model.get(0)
appLauncher.launchApp(firstApp)
}
event.accepted = true
} else if (event.key === Qt.Key_Down || event.key
=== Qt.Key_Up || event.key === Qt.Key_Left || event.key
=== Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
event.accepted = false
}
}
Connections {
function onShouldBeVisibleChanged() {
if (!appDrawerPopout.shouldBeVisible)
searchField.clearFocus()
}
target: appDrawerPopout
}
}
Row {
width: parent.width
height: 40
spacing: Theme.spacingM
visible: searchField.text.length === 0
Item {
width: 200
height: 36
DankDropdown {
anchors.fill: parent
text: ""
currentValue: appLauncher.selectedCategory
options: appLauncher.categories
optionIcons: appLauncher.categoryIcons
onValueChanged: function (value) {
appLauncher.setCategory(value)
}
}
}
Item {
width: parent.width - 300
height: 1
}
Row {
spacing: 4
anchors.verticalCenter: parent.verticalCenter
DankActionButton {
buttonSize: 36
circular: false
iconName: "view_list"
iconSize: 20
iconColor: appLauncher.viewMode
=== "list" ? Theme.primary : Theme.surfaceText
hoverColor: appLauncher.viewMode
=== "list" ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.08)
backgroundColor: appLauncher.viewMode
=== "list" ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
onClicked: {
appLauncher.setViewMode("list")
}
}
DankActionButton {
buttonSize: 36
circular: false
iconName: "grid_view"
iconSize: 20
iconColor: appLauncher.viewMode
=== "grid" ? Theme.primary : Theme.surfaceText
hoverColor: appLauncher.viewMode
=== "grid" ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.08)
backgroundColor: appLauncher.viewMode
=== "grid" ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
onClicked: {
appLauncher.setViewMode("grid")
}
}
}
}
Rectangle {
width: parent.width
height: {
let usedHeight = 40 + Theme.spacingL
usedHeight += 52 + Theme.spacingL
usedHeight += (searchField.text.length === 0 ? 40 + Theme.spacingL : 0)
return parent.height - usedHeight
}
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.1)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.05)
border.width: 1
DankListView {
id: appList
property int itemHeight: 72
property int iconSize: 56
property bool showDescription: true
property int itemSpacing: Theme.spacingS
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
signal keyboardNavigationReset
signal itemClicked(int index, var modelData)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= count)
return
var itemY = index * (itemHeight + itemSpacing)
var itemBottom = itemY + itemHeight
if (itemY < contentY)
contentY = itemY
else if (itemBottom > contentY + height)
contentY = itemBottom - height
}
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: appLauncher.viewMode === "list"
model: appLauncher.model
currentIndex: appLauncher.selectedIndex
clip: true
spacing: itemSpacing
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex)
}
onItemClicked: function (index, modelData) {
appLauncher.launchApp(modelData)
}
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
contextMenu.show(mouseX, mouseY, modelData)
}
onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AlwaysOn
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
delegate: Rectangle {
width: ListView.view.width
height: appList.itemHeight
radius: Theme.cornerRadius
color: ListView.isCurrentItem ? Theme.primaryPressed : listMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium
border.width: ListView.isCurrentItem ? 2 : 1
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingL
Item {
width: appList.iconSize
height: appList.iconSize
anchors.verticalCenter: parent.verticalCenter
IconImage {
id: listIconImg
anchors.fill: parent
source: (model.icon) ? Quickshell.iconPath(
model.icon,
SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
}
Rectangle {
anchors.fill: parent
visible: !listIconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.primarySelected
StyledText {
anchors.centerIn: parent
text: (model.name
&& model.name.length
> 0) ? model.name.charAt(
0).toUpperCase(
) : "A"
font.pixelSize: appList.iconSize * 0.4
color: Theme.primary
font.weight: Font.Bold
}
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - appList.iconSize - Theme.spacingL
spacing: Theme.spacingXS
StyledText {
width: parent.width
text: model.name || ""
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: model.comment || "Application"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
elide: Text.ElideRight
visible: appList.showDescription
&& model.comment
&& model.comment.length > 0
}
}
}
MouseArea {
id: listMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
z: 10
onEntered: {
if (appList.hoverUpdatesSelection
&& !appList.keyboardNavigationActive)
appList.currentIndex = index
}
onPositionChanged: {
appList.keyboardNavigationReset()
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
appList.itemClicked(
index, model)
} else if (mouse.button === Qt.RightButton) {
var panelPos = mapToItem(
contextMenu.parent,
mouse.x, mouse.y)
appList.itemRightClicked(
index, model,
panelPos.x,
panelPos.y)
}
}
}
}
}
DankGridView {
id: appGrid
property int currentIndex: appLauncher.selectedIndex
property int columns: 4
property bool adaptiveColumns: false
property int minCellWidth: 120
property int maxCellWidth: 160
property int cellPadding: 8
property real iconSizeRatio: 0.6
property int maxIconSize: 56
property int minIconSize: 32
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
property int baseCellHeight: baseCellWidth + 20
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
property int remainingSpace: width - (actualColumns * cellWidth)
signal keyboardNavigationReset
signal itemClicked(int index, var modelData)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= count)
return
var itemY = Math.floor(
index / actualColumns) * cellHeight
var itemBottom = itemY + cellHeight
if (itemY < contentY)
contentY = itemY
else if (itemBottom > contentY + height)
contentY = itemBottom - height
}
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: appLauncher.viewMode === "grid"
model: appLauncher.model
clip: true
cellWidth: baseCellWidth
cellHeight: baseCellHeight
leftMargin: Math.max(Theme.spacingS,
remainingSpace / 2)
rightMargin: leftMargin
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex)
}
onItemClicked: function (index, modelData) {
appLauncher.launchApp(modelData)
}
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
contextMenu.show(mouseX, mouseY, modelData)
}
onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
delegate: Rectangle {
width: appGrid.cellWidth - appGrid.cellPadding
height: appGrid.cellHeight - appGrid.cellPadding
radius: Theme.cornerRadius
color: appGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
border.color: appGrid.currentIndex === index ? Theme.primarySelected : Theme.outlineMedium
border.width: appGrid.currentIndex === index ? 2 : 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
Item {
property int iconSize: Math.min(
appGrid.maxIconSize,
Math.max(
appGrid.minIconSize,
appGrid.cellWidth
* appGrid.iconSizeRatio))
width: iconSize
height: iconSize
anchors.horizontalCenter: parent.horizontalCenter
IconImage {
id: gridIconImg
anchors.fill: parent
source: (model.icon) ? Quickshell.iconPath(
model.icon,
SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
}
Rectangle {
anchors.fill: parent
visible: !gridIconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.primarySelected
StyledText {
anchors.centerIn: parent
text: (model.name
&& model.name.length
> 0) ? model.name.charAt(
0).toUpperCase(
) : "A"
font.pixelSize: Math.min(
28,
parent.width * 0.5)
color: Theme.primary
font.weight: Font.Bold
}
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
width: appGrid.cellWidth - 12
text: model.name || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
maximumLineCount: 2
wrapMode: Text.WordWrap
}
}
MouseArea {
id: gridMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
z: 10
onEntered: {
if (appGrid.hoverUpdatesSelection
&& !appGrid.keyboardNavigationActive)
appGrid.currentIndex = index
}
onPositionChanged: {
appGrid.keyboardNavigationReset()
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
appGrid.itemClicked(
index, model)
} else if (mouse.button === Qt.RightButton) {
var panelPos = mapToItem(
contextMenu.parent,
mouse.x, mouse.y)
appGrid.itemRightClicked(
index, model,
panelPos.x,
panelPos.y)
}
}
}
}
}
}
}
}
}
}
Rectangle {
id: contextMenu
property var currentApp: null
property bool menuVisible: false
function show(x, y, app) {
currentApp = app
const menuWidth = 180
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
let finalX = x + 8
let finalY = y + 8
if (finalX + menuWidth > appDrawerPopout.popupWidth) {
finalX = x - menuWidth - 8
}
if (finalY + menuHeight > appDrawerPopout.popupHeight) {
finalY = y - menuHeight - 8
}
finalX = Math.max(
8, Math.min(finalX,
appDrawerPopout.popupWidth - menuWidth - 8))
finalY = Math.max(8, Math.min(
finalY,
appDrawerPopout.popupHeight - menuHeight - 8))
contextMenu.x = finalX
contextMenu.y = finalY
contextMenu.visible = true
contextMenu.menuVisible = true
}
function close() {
contextMenu.menuVisible = false
Qt.callLater(() => {
contextMenu.visible = false
})
}
visible: false
width: 180
height: menuColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
z: 1000
opacity: menuVisible ? 1 : 0
scale: menuVisible ? 1 : 0.85
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: pinMouseArea.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: {
if (!contextMenu.currentApp
|| !contextMenu.currentApp.desktopEntry)
return "push_pin"
var appId = contextMenu.currentApp.desktopEntry.id
|| contextMenu.currentApp.desktopEntry.execString
|| ""
return SessionData.isPinnedApp(
appId) ? "keep_off" : "push_pin"
}
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (!contextMenu.currentApp
|| !contextMenu.currentApp.desktopEntry)
return "Pin to Dock"
var appId = contextMenu.currentApp.desktopEntry.id
|| contextMenu.currentApp.desktopEntry.execString
|| ""
return SessionData.isPinnedApp(
appId) ? "Unpin from Dock" : "Pin to Dock"
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: pinMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!contextMenu.currentApp
|| !contextMenu.currentApp.desktopEntry)
return
var appId = contextMenu.currentApp.desktopEntry.id
|| contextMenu.currentApp.desktopEntry.execString
|| ""
if (SessionData.isPinnedApp(appId))
SessionData.removePinnedApp(appId)
else
SessionData.addPinnedApp(appId)
contextMenu.close()
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: launchMouseArea.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "launch"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Launch"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: launchMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (contextMenu.currentApp)
appLauncher.launchApp(contextMenu.currentApp)
contextMenu.close()
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
MouseArea {
anchors.fill: parent
visible: contextMenu.visible
z: 999
onClicked: {
contextMenu.close()
}
MouseArea {
x: contextMenu.x
y: contextMenu.y
width: contextMenu.width
height: contextMenu.height
onClicked: {
// Prevent closing when clicking on the menu itself
}
}
}
}
-195
View File
@@ -1,195 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property string searchQuery: ""
property string selectedCategory: "All"
property string viewMode: "list" // "list" or "grid"
property int selectedIndex: 0
property int maxResults: 50
property int gridColumns: 4
property bool debounceSearch: true
property int debounceInterval: 50
property bool keyboardNavigationActive: false
property var categories: {
var allCategories = AppSearchService.getAllCategories().filter(cat => {
return cat !== "Education"
&& cat !== "Science"
})
var result = ["All"]
return result.concat(allCategories.filter(cat => {
return cat !== "All"
}))
}
property var categoryIcons: categories.map(category => {
return AppSearchService.getCategoryIcon(
category)
})
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
property alias model: filteredModel
property var _watchApplications: AppSearchService.applications
signal appLaunched(var app)
signal categorySelected(string category)
signal viewModeSelected(string mode)
function updateFilteredModel() {
filteredModel.clear()
selectedIndex = 0
keyboardNavigationActive = false
var apps = []
if (searchQuery.length === 0) {
if (selectedCategory === "All") {
apps = AppSearchService.getAppsInCategory(
"All") // HACK: Use function call instead of property
} else {
var categoryApps = AppSearchService.getAppsInCategory(
selectedCategory)
apps = categoryApps.slice(0, maxResults)
}
} else {
if (selectedCategory === "All") {
apps = AppSearchService.searchApplications(searchQuery)
} else {
var categoryApps = AppSearchService.getAppsInCategory(
selectedCategory)
if (categoryApps.length > 0) {
var allSearchResults = AppSearchService.searchApplications(
searchQuery)
var categoryNames = new Set(categoryApps.map(app => {
return app.name
}))
apps = allSearchResults.filter(searchApp => {
return categoryNames.has(
searchApp.name)
}).slice(0, maxResults)
} else {
apps = []
}
}
}
if (searchQuery.length === 0)
apps = apps.sort(function (a, b) {
var aId = a.id || (a.execString || a.exec || "")
var bId = b.id || (b.execString || b.exec || "")
var aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0
var bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0
if (aUsage !== bUsage)
return bUsage - aUsage
return (a.name || "").localeCompare(b.name || "")
})
apps.forEach(app => {
if (app)
filteredModel.append({
"name": app.name || "",
"exec": app.execString || "",
"icon": app.icon
|| "application-x-executable",
"comment": app.comment || "",
"categories": app.categories
|| [],
"desktopEntry": app
})
})
}
function selectNext() {
if (filteredModel.count > 0) {
keyboardNavigationActive = true
if (viewMode === "grid") {
var newIndex = Math.min(selectedIndex + gridColumns,
filteredModel.count - 1)
selectedIndex = newIndex
} else {
selectedIndex = Math.min(selectedIndex + 1,
filteredModel.count - 1)
}
}
}
function selectPrevious() {
if (filteredModel.count > 0) {
keyboardNavigationActive = true
if (viewMode === "grid") {
var newIndex = Math.max(selectedIndex - gridColumns, 0)
selectedIndex = newIndex
} else {
selectedIndex = Math.max(selectedIndex - 1, 0)
}
}
}
function selectNextInRow() {
if (filteredModel.count > 0 && viewMode === "grid") {
keyboardNavigationActive = true
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1)
}
}
function selectPreviousInRow() {
if (filteredModel.count > 0 && viewMode === "grid") {
keyboardNavigationActive = true
selectedIndex = Math.max(selectedIndex - 1, 0)
}
}
function launchSelected() {
if (filteredModel.count > 0 && selectedIndex >= 0
&& selectedIndex < filteredModel.count) {
var selectedApp = filteredModel.get(selectedIndex)
launchApp(selectedApp)
}
}
function launchApp(appData) {
if (!appData)
return
appData.desktopEntry.execute()
appLaunched(appData)
AppUsageHistoryData.addAppUsage(appData.desktopEntry)
}
function setCategory(category) {
selectedCategory = category
categorySelected(category)
}
function setViewMode(mode) {
viewMode = mode
viewModeSelected(mode)
}
onSearchQueryChanged: {
if (debounceSearch)
searchDebounceTimer.restart()
else
updateFilteredModel()
}
onSelectedCategoryChanged: updateFilteredModel()
onAppUsageRankingChanged: updateFilteredModel()
on_WatchApplicationsChanged: updateFilteredModel()
Component.onCompleted: {
updateFilteredModel()
}
ListModel {
id: filteredModel
}
Timer {
id: searchDebounceTimer
interval: root.debounceInterval
repeat: false
onTriggered: updateFilteredModel()
}
}
-157
View File
@@ -1,157 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
Item {
id: root
property var categories: []
property string selectedCategory: "All"
property bool compact: false // For different layout styles
signal categorySelected(string category)
height: compact ? 36 : (72 + Theme.spacingS) // Single row vs two rows
Row {
visible: compact
width: parent.width
spacing: Theme.spacingS
Repeater {
model: categories.slice(0, Math.min(categories.length,
8)) // Limit for space
Rectangle {
height: 36
width: (parent.width - (Math.min(
categories.length,
8) - 1) * Theme.spacingS) / Math.min(
categories.length, 8)
radius: Theme.cornerRadius
color: selectedCategory === modelData ? Theme.primary : "transparent"
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(
Theme.outline.r,
Theme.outline.g,
Theme.outline.b,
0.3)
StyledText {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData
categorySelected(modelData)
}
}
}
}
}
Column {
visible: !compact
width: parent.width
spacing: Theme.spacingS
Row {
property var firstRowCategories: categories.slice(
0, Math.min(4,
categories.length))
width: parent.width
spacing: Theme.spacingS
Repeater {
model: parent.firstRowCategories
Rectangle {
height: 36
width: (parent.width - (parent.firstRowCategories.length - 1)
* Theme.spacingS) / parent.firstRowCategories.length
radius: Theme.cornerRadius
color: selectedCategory === modelData ? Theme.primary : "transparent"
border.color: selectedCategory
=== modelData ? "transparent" : Qt.rgba(
Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.3)
StyledText {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData
categorySelected(modelData)
}
}
}
}
}
Row {
property var secondRowCategories: categories.slice(
4, categories.length)
width: parent.width
spacing: Theme.spacingS
visible: secondRowCategories.length > 0
Repeater {
model: parent.secondRowCategories
Rectangle {
height: 36
width: (parent.width - (parent.secondRowCategories.length - 1)
* Theme.spacingS) / parent.secondRowCategories.length
radius: Theme.cornerRadius
color: selectedCategory === modelData ? Theme.primary : "transparent"
border.color: selectedCategory
=== modelData ? "transparent" : Qt.rgba(
Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.3)
StyledText {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData
categorySelected(modelData)
}
}
}
}
}
}
}
-276
View File
@@ -1,276 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: calendarGrid
property date displayDate: new Date()
property date selectedDate: new Date()
function loadEventsForMonth() {
if (!CalendarService || !CalendarService.khalAvailable)
return
let firstDay = new Date(displayDate.getFullYear(),
displayDate.getMonth(), 1)
let dayOfWeek = firstDay.getDay()
let startDate = new Date(firstDay)
startDate.setDate(startDate.getDate(
) - dayOfWeek - 7) // Extra week padding
let lastDay = new Date(displayDate.getFullYear(),
displayDate.getMonth() + 1, 0)
let endDate = new Date(lastDay)
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay(
)) + 7) // Extra week padding
CalendarService.loadEvents(startDate, endDate)
}
spacing: Theme.spacingM
onDisplayDateChanged: {
loadEventsForMonth()
}
Component.onCompleted: {
loadEventsForMonth()
}
Connections {
function onKhalAvailableChanged() {
if (CalendarService && CalendarService.khalAvailable)
loadEventsForMonth()
}
target: CalendarService
enabled: CalendarService !== null
}
Row {
width: parent.width
height: 40
Rectangle {
width: 40
height: 40
radius: Theme.cornerRadius
color: prevMonthArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "chevron_left"
size: Theme.iconSize
color: Theme.primary
}
MouseArea {
id: prevMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(displayDate)
newDate.setMonth(newDate.getMonth() - 1)
displayDate = newDate
}
}
}
StyledText {
width: parent.width - 80
height: 40
text: Qt.formatDate(displayDate, "MMMM yyyy")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Rectangle {
width: 40
height: 40
radius: Theme.cornerRadius
color: nextMonthArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "chevron_right"
size: Theme.iconSize
color: Theme.primary
}
MouseArea {
id: nextMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(displayDate)
newDate.setMonth(newDate.getMonth() + 1)
displayDate = newDate
}
}
}
}
Row {
width: parent.width
height: 32
Repeater {
model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
Rectangle {
width: parent.width / 7
height: 32
color: "transparent"
StyledText {
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
}
}
}
Grid {
property date firstDay: {
let date = new Date(displayDate.getFullYear(),
displayDate.getMonth(), 1)
let dayOfWeek = date.getDay()
date.setDate(date.getDate() - dayOfWeek)
return date
}
width: parent.width
height: 200 // Fixed height for calendar
columns: 7
rows: 6
Repeater {
model: 42
Rectangle {
property date dayDate: {
let date = new Date(parent.firstDay)
date.setDate(date.getDate() + index)
return date
}
property bool isCurrentMonth: dayDate.getMonth(
) === displayDate.getMonth()
property bool isToday: dayDate.toDateString(
) === new Date().toDateString()
property bool isSelected: dayDate.toDateString(
) === selectedDate.toDateString()
width: parent.width / 7
height: parent.height / 6
color: "transparent"
clip: true
Rectangle {
anchors.centerIn: parent
width: parent.width - 4
height: parent.height - 4
color: isSelected ? Theme.primary : isToday ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius
clip: true
StyledText {
anchors.centerIn: parent
text: dayDate.getDate()
font.pixelSize: Theme.fontSizeMedium
color: isSelected ? Theme.surface : isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
font.weight: isToday
|| isSelected ? Font.Medium : Font.Normal
}
Rectangle {
id: eventIndicator
anchors.fill: parent
radius: parent.radius
visible: CalendarService
&& CalendarService.khalAvailable
&& CalendarService.hasEventsForDate(dayDate)
opacity: {
if (isSelected)
return 0.9
else if (isToday)
return 0.8
else
return 0.6
}
gradient: Gradient {
GradientStop {
position: 0.89
color: "transparent"
}
GradientStop {
position: 0.9
color: {
if (isSelected)
return Qt.lighter(Theme.primary, 1.3)
else if (isToday)
return Theme.primary
else
return Theme.primary
}
}
GradientStop {
position: 1
color: {
if (isSelected)
return Qt.lighter(Theme.primary, 1.3)
else if (isToday)
return Theme.primary
else
return Theme.primary
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
MouseArea {
id: dayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedDate = dayDate
}
}
}
}
}
}
-326
View File
@@ -1,326 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Services.Mpris
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules.CentcomCenter
import qs.Services
PanelWindow {
id: root
readonly property bool hasActiveMedia: MprisController.activePlayer !== null
property bool calendarVisible: false
property bool shouldBeVisible: false
property real triggerX: (Screen.width - 480) / 2
property real triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + 4
property real triggerWidth: 80
property string triggerSection: "center"
property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen) {
triggerX = x
triggerY = y
triggerWidth = width
triggerSection = section
triggerScreen = screen
}
visible: calendarVisible || closeTimer.running
screen: triggerScreen
onCalendarVisibleChanged: {
if (calendarVisible) {
closeTimer.stop()
shouldBeVisible = true
visible = true
Qt.callLater(() => {
calendarGrid.loadEventsForMonth()
})
} else {
shouldBeVisible = false
closeTimer.restart()
}
}
Timer {
id: closeTimer
interval: Theme.mediumDuration + 50
onTriggered: {
if (!shouldBeVisible) {
visible = false
}
}
}
onVisibleChanged: {
if (visible && calendarGrid)
calendarGrid.loadEventsForMonth()
}
implicitWidth: 480
implicitHeight: 600
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: shouldBeVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
Rectangle {
id: mainContainer
readonly property real targetWidth: Math.min(
(root.screen ? root.screen.width : Screen.width)
* 0.9, 600)
function calculateWidth() {
let baseWidth = 320
if (leftWidgets.hasAnyWidgets)
return Math.min(parent.width * 0.9, 600)
return Math.min(parent.width * 0.7, 400)
}
function calculateHeight() {
let contentHeight = Theme.spacingM * 2
// margins
let widgetHeight = 160
widgetHeight += 140 + Theme.spacingM
let calendarHeight = 300
let mainRowHeight = Math.max(widgetHeight, calendarHeight)
contentHeight += mainRowHeight + Theme.spacingM
if (CalendarService && CalendarService.khalAvailable) {
let hasEvents = events.selectedDateEvents
&& events.selectedDateEvents.length > 0
let eventsHeight = hasEvents ? Math.min(
300,
80 + events.selectedDateEvents.length * 60) : 120
contentHeight += eventsHeight
} else {
contentHeight -= Theme.spacingM
}
return Math.min(contentHeight, parent.height * 0.9)
}
readonly property real calculatedX: {
var screenWidth = root.screen ? root.screen.width : Screen.width
if (root.triggerSection === "center") {
return (screenWidth - targetWidth) / 2
}
var centerX = root.triggerX + (root.triggerWidth / 2) - (targetWidth / 2)
if (centerX >= Theme.spacingM
&& centerX + targetWidth <= screenWidth - Theme.spacingM) {
return centerX
}
if (centerX < Theme.spacingM) {
return Theme.spacingM
}
if (centerX + targetWidth > screenWidth - Theme.spacingM) {
return screenWidth - targetWidth - Theme.spacingM
}
return centerX
}
width: targetWidth
height: calculateHeight()
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
opacity: shouldBeVisible ? 1 : 0
scale: shouldBeVisible ? 1 : 0.9
x: calculatedX
y: root.triggerY
onOpacityChanged: {
if (opacity === 1)
Qt.callLater(() => {
height = calculateHeight()
})
}
Connections {
function onEventsByDateChanged() {
if (mainContainer.opacity === 1)
mainContainer.height = mainContainer.calculateHeight()
}
function onKhalAvailableChanged() {
if (mainContainer.opacity === 1)
mainContainer.height = mainContainer.calculateHeight()
}
target: CalendarService
enabled: CalendarService !== null
}
Connections {
function onSelectedDateEventsChanged() {
if (mainContainer.opacity === 1)
mainContainer.height = mainContainer.calculateHeight()
}
target: events
enabled: events !== null
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g,
Theme.surfaceTint.b, 0.04)
radius: parent.radius
SequentialAnimation on opacity {
running: shouldBeVisible
loops: Animation.Infinite
NumberAnimation {
to: 0.08
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
to: 0.02
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
focus: true
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
calendarVisible = false
event.accepted = true
} else {
// Don't handle other keys - let them bubble up to modals
event.accepted = false
}
}
Row {
width: parent.width
height: {
let widgetHeight = 160
// Media widget
widgetHeight += 140 + Theme.spacingM // Weather/SystemInfo widget with spacing
let calendarHeight = 300
// Calendar
return Math.max(widgetHeight, calendarHeight)
}
spacing: Theme.spacingM
Column {
id: leftWidgets
property bool hasAnyWidgets: true
width: hasAnyWidgets ? parent.width
* 0.42 : 0 // Slightly narrower for better proportions
height: childrenRect.height
spacing: Theme.spacingM
visible: hasAnyWidgets
anchors.top: parent.top
MediaPlayer {
width: parent.width
height: 160
}
Weather {
width: parent.width
height: 140
visible: SettingsData.weatherEnabled
}
SystemInfo {
width: parent.width
height: 140
visible: !SettingsData.weatherEnabled
}
}
Rectangle {
width: leftWidgets.hasAnyWidgets ? parent.width - leftWidgets.width
- Theme.spacingM : parent.width
height: parent.height
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.2)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
CalendarGrid {
id: calendarGrid
anchors.fill: parent
anchors.margins: Theme.spacingS
}
}
}
Events {
id: events
width: parent.width
selectedDate: calendarGrid.selectedDate
}
}
Behavior on opacity {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.15)
shadowOpacity: 0.15
}
}
MouseArea {
anchors.fill: parent
z: -1
enabled: shouldBeVisible
onClicked: function (mouse) {
var localPos = mapToItem(mainContainer, mouse.x, mouse.y)
if (localPos.x < 0 || localPos.x > mainContainer.width
|| localPos.y < 0 || localPos.y > mainContainer.height)
calendarVisible = false
}
}
}
-353
View File
@@ -1,353 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: events
property date selectedDate: new Date()
property var selectedDateEvents: []
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
property bool shouldShow: CalendarService && CalendarService.khalAvailable
function updateSelectedDateEvents() {
if (CalendarService && CalendarService.khalAvailable) {
let events = CalendarService.getEventsForDate(selectedDate)
selectedDateEvents = events
} else {
selectedDateEvents = []
}
}
onSelectedDateEventsChanged: {
eventsList.model = selectedDateEvents
}
width: parent.width
height: shouldShow ? (hasEvents ? Math.min(
300,
80 + selectedDateEvents.length * 60) : 120) : 0
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.12)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
visible: shouldShow
layer.enabled: true
Component.onCompleted: {
updateSelectedDateEvents()
}
onSelectedDateChanged: {
updateSelectedDateEvents()
}
Connections {
function onEventsByDateChanged() {
updateSelectedDateEvents()
}
function onKhalAvailableChanged() {
updateSelectedDateEvents()
}
target: CalendarService
enabled: CalendarService !== null
}
Row {
id: headerRow
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
spacing: Theme.spacingS
DankIcon {
name: "event"
size: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: hasEvents ? (Qt.formatDate(selectedDate, "MMM d") + " • "
+ (selectedDateEvents.length
=== 1 ? "1 event" : selectedDateEvents.length
+ " events")) : Qt.formatDate(
selectedDate, "MMM d")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
visible: !hasEvents
DankIcon {
name: "event_busy"
size: Theme.iconSize + 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "No events"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.5)
font.weight: Font.Normal
anchors.horizontalCenter: parent.horizontalCenter
}
}
DankListView {
id: eventsList
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingL
anchors.topMargin: Theme.spacingM
visible: opacity > 0
opacity: hasEvents ? 1 : 0
clip: true
spacing: Theme.spacingS
boundsBehavior: Flickable.StopAtBounds
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
interactive: true
flickDeceleration: 1500
maximumFlickVelocity: 2000
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
property real momentum: 0
onWheel: event => {
if (event.pixelDelta.y !== 0) {
// Touchpad with pixel delta
momentum = event.pixelDelta.y * 1.8
} else {
// Mouse wheel with angle delta
momentum = (event.angleDelta.y / 120)
* (60 * 2.5) // ~2.5 items per wheel step
}
let newY = parent.contentY - momentum
newY = Math.max(
0, Math.min(parent.contentHeight - parent.height,
newY))
parent.contentY = newY
momentum *= 0.92 // Decay for smooth momentum
event.accepted = true
}
}
ScrollBar.vertical: ScrollBar {
policy: eventsList.contentHeight
> eventsList.height ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
delegate: Rectangle {
width: eventsList.width
height: eventContent.implicitHeight + Theme.spacingM
radius: Theme.cornerRadius
color: {
if (modelData.url && eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12)
else if (eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.06)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.06)
}
border.color: {
if (modelData.url && eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.3)
else if (eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.15)
return "transparent"
}
border.width: 1
Rectangle {
width: 4
height: parent.height - 8
anchors.left: parent.left
anchors.leftMargin: 4
anchors.verticalCenter: parent.verticalCenter
radius: 2
color: Theme.primary
opacity: 0.8
}
Column {
id: eventContent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingL + 4
anchors.rightMargin: Theme.spacingM
spacing: 6
StyledText {
width: parent.width
text: modelData.title
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
}
Item {
width: parent.width
height: Math.max(timeRow.height, locationRow.height)
Row {
id: timeRow
spacing: 4
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "schedule"
size: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (modelData.allDay) {
return "All day"
} else {
let timeFormat = SettingsData.use24HourClock ? "H:mm" : "h:mm AP"
let startTime = Qt.formatTime(
modelData.start, timeFormat)
if (modelData.start.toDateString(
) !== modelData.end.toDateString(
) || modelData.start.getTime(
) !== modelData.end.getTime())
return startTime + " " + Qt.formatTime(
modelData.end, timeFormat)
return startTime
}
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
id: locationRow
spacing: 4
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: modelData.location !== ""
DankIcon {
name: "location_on"
size: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.location
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
maximumLineCount: 1
width: Math.min(implicitWidth, 200)
}
}
}
}
MouseArea {
id: eventMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData.url !== ""
onClicked: {
if (modelData.url && modelData.url !== "") {
if (Qt.openUrlExternally(modelData.url) === false)
console.warn("Failed to open URL: " + modelData.url)
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.25
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
-471
View File
@@ -1,471 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Services.Mpris
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: mediaPlayer
property MprisPlayer activePlayer: MprisController.activePlayer
property string lastValidTitle: ""
property string lastValidArtist: ""
property string lastValidAlbum: ""
property string lastValidArtUrl: ""
property real currentPosition: activePlayer
&& activePlayer.positionSupported ? activePlayer.position : 0
function ratio() {
if (!activePlayer || activePlayer.length <= 0) {
return 0
}
let calculatedRatio = currentPosition / activePlayer.length
return Math.max(0, Math.min(1, calculatedRatio))
}
onActivePlayerChanged: {
if (activePlayer && activePlayer.positionSupported) {
activePlayer.positionChanged()
}
}
width: parent.width
height: parent.height
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
Timer {
id: positionTimer
interval: 500
running: activePlayer
&& activePlayer.playbackState === MprisPlaybackState.Playing
&& !progressMouseArea.isSeeking
repeat: true
onTriggered: {
if (activePlayer && activePlayer.positionSupported) {
activePlayer.positionChanged()
}
}
}
Timer {
id: cleanupTimer
interval: 2000
running: !activePlayer
onTriggered: {
lastValidTitle = ""
lastValidArtist = ""
lastValidAlbum = ""
lastValidArtUrl = ""
currentPosition = 0
stop()
}
}
Connections {
function onTrackChanged() {
if (activePlayer && activePlayer.positionSupported) {
activePlayer.positionChanged()
}
}
target: activePlayer
}
Item {
anchors.fill: parent
anchors.margins: Theme.spacingS
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
visible: (!activePlayer && !lastValidTitle)
|| (activePlayer && activePlayer.trackTitle === ""
&& lastValidTitle === "")
DankIcon {
name: "music_note"
size: Theme.iconSize + 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "No Media Playing"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
anchors.fill: parent
spacing: Theme.spacingS
visible: activePlayer && activePlayer.trackTitle !== ""
|| lastValidTitle !== ""
Row {
width: parent.width
height: 60
spacing: Theme.spacingM
Rectangle {
width: 60
height: 60
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
Item {
anchors.fill: parent
clip: true
Image {
id: albumArt
anchors.fill: parent
source: activePlayer && activePlayer.trackArtUrl
|| lastValidArtUrl || ""
onSourceChanged: {
if (activePlayer && activePlayer.trackArtUrl)
lastValidArtUrl = activePlayer.trackArtUrl
}
fillMode: Image.PreserveAspectCrop
smooth: true
cache: true
}
Rectangle {
anchors.fill: parent
visible: albumArt.status !== Image.Ready
color: "transparent"
DankIcon {
anchors.centerIn: parent
name: "album"
size: 28
color: Theme.surfaceVariantText
}
}
}
}
Column {
width: parent.width - 60 - Theme.spacingM
height: parent.height
spacing: Theme.spacingXS
StyledText {
text: activePlayer && activePlayer.trackTitle
|| lastValidTitle || "Unknown Track"
onTextChanged: {
if (activePlayer && activePlayer.trackTitle)
lastValidTitle = activePlayer.trackTitle
}
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
}
StyledText {
text: activePlayer && activePlayer.trackArtist
|| lastValidArtist || "Unknown Artist"
onTextChanged: {
if (activePlayer && activePlayer.trackArtist)
lastValidArtist = activePlayer.trackArtist
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
}
StyledText {
text: activePlayer && activePlayer.trackAlbum
|| lastValidAlbum || ""
onTextChanged: {
if (activePlayer && activePlayer.trackAlbum)
lastValidAlbum = activePlayer.trackAlbum
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.6)
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
visible: text.length > 0
}
}
}
Item {
id: progressBarContainer
width: parent.width
height: 24
Rectangle {
id: progressBarBackground
width: parent.width
height: 6
radius: 3
color: Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
visible: activePlayer !== null
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: progressFill
height: parent.height
radius: parent.radius
color: Theme.primary
width: Math.max(0, Math.min(parent.width,
parent.width * ratio()))
Behavior on width {
NumberAnimation {
duration: 100
}
}
}
Rectangle {
id: progressHandle
width: 12
height: 12
radius: 6
color: Theme.primary
border.color: Qt.lighter(Theme.primary, 1.3)
border.width: 1
x: Math.max(0, Math.min(parent.width - width,
progressFill.width - width / 2))
anchors.verticalCenter: parent.verticalCenter
visible: activePlayer && activePlayer.length > 0
scale: progressMouseArea.containsMouse
|| progressMouseArea.pressed ? 1.2 : 1
Behavior on scale {
NumberAnimation {
duration: 150
}
}
}
}
MouseArea {
id: progressMouseArea
property bool isSeeking: false
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.length > 0
&& activePlayer.canSeek
preventStealing: true
onPressed: function (mouse) {
isSeeking = true
if (activePlayer && activePlayer.length > 0
&& activePlayer.canSeek) {
let ratio = Math.max(
0, Math.min(
1,
mouse.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
}
}
onReleased: {
isSeeking = false
}
onPositionChanged: function (mouse) {
if (pressed && isSeeking && activePlayer
&& activePlayer.length > 0
&& activePlayer.canSeek) {
let ratio = Math.max(
0, Math.min(
1,
mouse.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
}
}
onClicked: function (mouse) {
if (activePlayer && activePlayer.length > 0
&& activePlayer.canSeek) {
let ratio = Math.max(
0, Math.min(
1,
mouse.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
}
}
}
MouseArea {
id: progressGlobalMouseArea
x: 0
y: 0
width: mediaPlayer.width
height: mediaPlayer.height
enabled: progressMouseArea.isSeeking
visible: false
preventStealing: true
onPositionChanged: function (mouse) {
if (progressMouseArea.isSeeking && activePlayer
&& activePlayer.length > 0
&& activePlayer.canSeek) {
let globalPos = mapToItem(progressBarBackground,
mouse.x, mouse.y)
let ratio = Math.max(
0, Math.min(
1,
globalPos.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
}
}
onReleased: {
progressMouseArea.isSeeking = false
}
}
}
Item {
width: parent.width
height: 32
visible: activePlayer !== null
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
height: parent.height
Rectangle {
width: 28
height: 28
radius: 14
color: prevBtnArea.containsMouse ? Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: 16
color: Theme.surfaceText
}
MouseArea {
id: prevBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!activePlayer)
return
if (activePlayer.position > 8
&& activePlayer.canSeek) {
activePlayer.position = 0
} else {
activePlayer.previous()
}
}
}
}
Rectangle {
width: 32
height: 32
radius: 16
color: Theme.primary
DankIcon {
anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState
=== MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 20
color: Theme.background
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer
&& activePlayer.togglePlaying()
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: nextBtnArea.containsMouse ? Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: 16
color: Theme.surfaceText
}
MouseArea {
id: nextBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.next()
}
}
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
}
-137
View File
@@ -1,137 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
Ref {
service: DgopService
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingM
SystemLogo {
width: 48
height: 48
}
Column {
width: parent.width - 48 - Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: DgopService.hostname
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: DgopService.distribution + " • " + DgopService.architecture
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.1)
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: "Uptime " + formatUptime(UserInfoService.uptime)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: "Load: " + DgopService.loadAverage
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: DgopService.processCount + " proc, " + DgopService.threadCount + " threads"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
}
}
}
function formatUptime(uptime) {
if (!uptime)
return "0m"
// Parse the uptime string - handle formats like "1 week, 4 days, 3:45" or "4 days, 3:45" or "3:45"
var uptimeStr = uptime.toString().trim()
// Check for weeks and days - need to add them together
var weekMatch = uptimeStr.match(/(\d+)\s+weeks?/)
var dayMatch = uptimeStr.match(/(\d+)\s+days?/)
if (weekMatch) {
var weeks = parseInt(weekMatch[1])
var totalDays = weeks * 7
if (dayMatch) {
var days = parseInt(dayMatch[1])
totalDays += days
}
return totalDays + "d"
} else if (dayMatch) {
var days = parseInt(dayMatch[1])
return days + "d"
}
// If it's just hours:minutes, show the largest unit
var timeMatch = uptimeStr.match(/(\d+):(\d+)/)
if (timeMatch) {
var hours = parseInt(timeMatch[1])
var minutes = parseInt(timeMatch[2])
if (hours > 0) {
return hours + "h"
} else {
return minutes + "m"
}
}
// Fallback - return as is but truncated
return uptimeStr.length > 8 ? uptimeStr.substring(0,
8) + "…" : uptimeStr
}
}
-236
View File
@@ -1,236 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: weather
width: parent.width
height: parent.height
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
Ref {
service: WeatherService
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
visible: !WeatherService.weather.available
|| WeatherService.weather.temp === 0
DankIcon {
name: "cloud_off"
size: Theme.iconSize + 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "No Weather Data"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingS
visible: WeatherService.weather.available
&& WeatherService.weather.temp !== 0
Item {
width: parent.width
height: 60
DankIcon {
id: refreshButton
name: "refresh"
size: Theme.iconSize - 6
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: -Theme.spacingS
anchors.topMargin: -Theme.spacingS
property bool isRefreshing: false
enabled: !isRefreshing
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
onClicked: {
refreshButton.isRefreshing = true
WeatherService.forceRefresh()
refreshTimer.restart()
}
enabled: parent.enabled
}
Timer {
id: refreshTimer
interval: 2000
onTriggered: refreshButton.isRefreshing = false
}
NumberAnimation on rotation {
running: refreshButton.isRefreshing
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
DankIcon {
name: WeatherService.getWeatherIcon(
WeatherService.weather.wCode)
size: Theme.iconSize + 8
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp)
+ "°" + (SettingsData.useFahrenheit ? "F" : "C")
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Light
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (WeatherService.weather.available)
SettingsData.setTemperatureUnit(
!SettingsData.useFahrenheit)
}
enabled: WeatherService.weather.available
}
}
StyledText {
text: WeatherService.weather.city || ""
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
}
}
Grid {
columns: 2
spacing: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: Theme.spacingXS
DankIcon {
name: "humidity_low"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: WeatherService.weather.humidity ? WeatherService.weather.humidity
+ "%" : "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
DankIcon {
name: "air"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: WeatherService.weather.wind || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
DankIcon {
name: "wb_twilight"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: WeatherService.weather.sunrise || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
DankIcon {
name: "bedtime"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: WeatherService.weather.sunset || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
}
@@ -1,150 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: root
property string currentSinkDisplayName: AudioService.sink ? AudioService.displayName(
AudioService.sink) : ""
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Output Device"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Rectangle {
width: parent.width
height: 35
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.3)
border.width: 1
visible: AudioService.sink !== null
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "check_circle"
size: Theme.iconSize - 4
color: Theme.primary
}
StyledText {
text: "Current: " + (root.currentSinkDisplayName || "None")
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium
}
}
}
Repeater {
model: {
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values)
return []
let sinks = []
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
let node = Pipewire.nodes.values[i]
if (!node || node.isStream)
continue
if ((node.type & PwNodeType.AudioSink) === PwNodeType.AudioSink)
sinks.push(node)
}
return sinks
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: deviceArea.containsMouse ? Qt.rgba(
Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.08) : (modelData === AudioService.sink ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData === AudioService.sink ? Theme.primary : "transparent"
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: {
if (modelData.name.includes("bluez"))
return "headset"
else if (modelData.name.includes("hdmi"))
return "tv"
else if (modelData.name.includes("usb"))
return "headset"
else
return "speaker"
}
size: Theme.iconSize
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
}
StyledText {
text: {
if (AudioService.subtitle(modelData.name)
&& AudioService.subtitle(
modelData.name) !== "")
return AudioService.subtitle(modelData.name)
+ (modelData === AudioService.sink ? " • Selected" : "")
else
return modelData === AudioService.sink ? "Selected" : ""
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
visible: text !== ""
}
}
}
MouseArea {
id: deviceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData)
Pipewire.preferredDefaultAudioSink = modelData
}
}
}
}
}
@@ -1,149 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: root
property string currentSourceDisplayName: AudioService.source ? AudioService.displayName(
AudioService.source) : ""
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Input Device"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Rectangle {
width: parent.width
height: 35
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.3)
border.width: 1
visible: AudioService.source !== null
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "check_circle"
size: Theme.iconSize - 4
color: Theme.primary
}
StyledText {
text: "Current: " + (root.currentSourceDisplayName || "None")
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium
}
}
}
Repeater {
model: {
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values)
return []
let sources = []
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
let node = Pipewire.nodes.values[i]
if (!node || node.isStream)
continue
if ((node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource
&& !node.name.includes(".monitor"))
sources.push(node)
}
return sources
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: sourceArea.containsMouse ? Qt.rgba(
Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.08) : (modelData === AudioService.source ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData === AudioService.source ? Theme.primary : "transparent"
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: {
if (modelData.name.includes("bluez"))
return "headset_mic"
else if (modelData.name.includes("usb"))
return "headset_mic"
else
return "mic"
}
size: Theme.iconSize
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
font.weight: modelData === AudioService.source ? Font.Medium : Font.Normal
}
StyledText {
text: {
if (AudioService.subtitle(modelData.name)
&& AudioService.subtitle(
modelData.name) !== "")
return AudioService.subtitle(modelData.name)
+ (modelData === AudioService.source ? " • Selected" : "")
else
return modelData === AudioService.source ? "Selected" : ""
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
visible: text !== ""
}
}
}
MouseArea {
id: sourceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData)
Pipewire.preferredDefaultAudioSource = modelData
}
}
}
}
}
@@ -1,239 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: root
property real micLevel: Math.min(100,
(AudioService.source
&& AudioService.source.audio
&& AudioService.source.audio.volume * 100)
|| 0)
property bool micMuted: (AudioService.source && AudioService.source.audio
&& AudioService.source.audio.muted) || false
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Microphone Level"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: root.micMuted ? "mic_off" : "mic"
size: Theme.iconSize
color: root.micMuted ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (AudioService.source && AudioService.source.audio)
AudioService.source.audio.muted = !AudioService.source.audio.muted
}
}
}
Item {
id: micSliderContainer
width: parent.width - 80
height: 32
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: micSliderTrack
width: parent.width
height: 8
radius: 4
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: micSliderFill
width: parent.width * (root.micLevel / 100)
height: parent.height
radius: parent.radius
color: Theme.primary
Behavior on width {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardDecel
}
}
}
Rectangle {
id: micHandle
width: 18
height: 18
radius: 9
color: Theme.primary
border.color: Qt.lighter(Theme.primary, 1.3)
border.width: 2
x: Math.max(0, Math.min(parent.width - width,
micSliderFill.width - width / 2))
anchors.verticalCenter: parent.verticalCenter
scale: micMouseArea.containsMouse
|| micMouseArea.pressed ? 1.2 : 1
Rectangle {
id: micTooltip
width: tooltipText.contentWidth + Theme.spacingS * 2
height: tooltipText.contentHeight + Theme.spacingXS * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
visible: (micMouseArea.containsMouse && !root.micMuted)
|| micMouseArea.isDragging
opacity: visible ? 1 : 0
StyledText {
id: tooltipText
text: Math.round(root.micLevel) + "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
}
}
MouseArea {
id: micMouseArea
property bool isDragging: false
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
preventStealing: true
onPressed: mouse => {
isDragging = true
let ratio = Math.max(
0, Math.min(1,
mouse.x / micSliderTrack.width))
let newMicLevel = Math.round(ratio * 100)
if (AudioService.source
&& AudioService.source.audio) {
AudioService.source.audio.muted = false
AudioService.source.audio.volume = newMicLevel / 100
}
}
onReleased: {
isDragging = false
}
onPositionChanged: mouse => {
if (pressed && isDragging) {
let ratio = Math.max(
0, Math.min(
1,
mouse.x / micSliderTrack.width))
let newMicLevel = Math.max(
0, Math.min(100, Math.round(
ratio * 100)))
if (AudioService.source
&& AudioService.source.audio) {
AudioService.source.audio.muted = false
AudioService.source.audio.volume = newMicLevel / 100
}
}
}
onClicked: mouse => {
let ratio = Math.max(
0, Math.min(1,
mouse.x / micSliderTrack.width))
let newMicLevel = Math.round(ratio * 100)
if (AudioService.source
&& AudioService.source.audio) {
AudioService.source.audio.muted = false
AudioService.source.audio.volume = newMicLevel / 100
}
}
}
MouseArea {
id: micGlobalMouseArea
x: 0
y: 0
width: root.parent ? root.parent.width : 0
height: root.parent ? root.parent.height : 0
enabled: micMouseArea.isDragging
visible: false
preventStealing: true
onPositionChanged: mouse => {
if (micMouseArea.isDragging) {
let globalPos = mapToItem(
micSliderTrack, mouse.x, mouse.y)
let ratio = Math.max(
0, Math.min(
1,
globalPos.x / micSliderTrack.width))
let newMicLevel = Math.max(
0, Math.min(100, Math.round(
ratio * 100)))
if (AudioService.source
&& AudioService.source.audio) {
AudioService.source.audio.muted = false
AudioService.source.audio.volume = newMicLevel / 100
}
}
}
onReleased: {
micMouseArea.isDragging = false
}
}
}
DankIcon {
name: "mic"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
@@ -1,69 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: root
property real volumeLevel: Math.min(
100,
(AudioService.sink && AudioService.sink.audio
&& AudioService.sink.audio.volume * 100)
|| 0)
property bool volumeMuted: (AudioService.sink && AudioService.sink.audio
&& AudioService.sink.audio.muted) || false
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Volume"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
id: volumeSlider
width: parent.width
minimum: 0
maximum: 100
leftIcon: root.volumeMuted ? "volume_off" : "volume_down"
rightIcon: "volume_up"
enabled: !root.volumeMuted
showValue: true
unit: "%"
Connections {
target: AudioService.sink
&& AudioService.sink.audio ? AudioService.sink.audio : null
function onVolumeChanged() {
volumeSlider.value = Math.round(
AudioService.sink.audio.volume * 100)
}
}
Component.onCompleted: {
if (AudioService.sink && AudioService.sink.audio) {
value = Math.round(AudioService.sink.audio.volume * 100)
}
let leftIconItem = volumeSlider.children[0].children[0]
if (leftIconItem) {
let mouseArea = Qt.createQmlObject(
'import QtQuick; import qs.Services; MouseArea { anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onClicked: { if (AudioService.sink && AudioService.sink.audio) AudioService.sink.audio.muted = !AudioService.sink.audio.muted; } }',
leftIconItem, "dynamicMouseArea")
}
}
onSliderValueChanged: newValue => {
if (AudioService.sink
&& AudioService.sink.audio) {
AudioService.sink.audio.muted = false
AudioService.sink.audio.volume = newValue / 100
}
}
}
}
-128
View File
@@ -1,128 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import Quickshell.Widgets
import qs.Common
import qs.Modules.ControlCenter.Audio
import qs.Services
import qs.Widgets
Item {
id: audioTab
property int audioSubTab: 0
Column {
anchors.fill: parent
spacing: Theme.spacingM
DankTabBar {
width: parent.width
tabHeight: 40
currentIndex: audioTab.audioSubTab
showIcons: false
model: [{
"text": "Output"
}, {
"text": "Input"
}]
onTabClicked: function (index) {
audioTab.audioSubTab = index
}
}
// Single Loader that switches between Output and Input
Loader {
width: parent.width
height: parent.height - 48
asynchronous: true
sourceComponent: audioTab.audioSubTab === 0 ? outputTabComponent : inputTabComponent
}
}
// Output Tab Component
Component {
id: outputTabComponent
DankFlickable {
clip: true
contentHeight: outputColumn.height
contentWidth: width
Column {
id: outputColumn
width: parent.width
spacing: Theme.spacingL
Loader {
width: parent.width
sourceComponent: volumeComponent
}
Loader {
width: parent.width
sourceComponent: outputDevicesComponent
}
}
}
}
// Input Tab Component
Component {
id: inputTabComponent
DankFlickable {
clip: true
contentHeight: inputColumn.height
contentWidth: width
Column {
id: inputColumn
width: parent.width
spacing: Theme.spacingL
Loader {
width: parent.width
sourceComponent: microphoneComponent
}
Loader {
width: parent.width
sourceComponent: inputDevicesComponent
}
}
}
}
// Volume Control Component
Component {
id: volumeComponent
VolumeControl {
width: parent.width
}
}
// Microphone Control Component
Component {
id: microphoneComponent
MicrophoneControl {
width: parent.width
}
}
// Output Devices Component
Component {
id: outputDevicesComponent
AudioDevicesList {
width: parent.width
}
}
// Input Devices Component
Component {
id: inputDevicesComponent
AudioInputDevicesList {
width: parent.width
}
}
}
@@ -1,452 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: root
width: parent.width
spacing: Theme.spacingM
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Row {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Available Devices"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - scanButton.width - parent.spacing
- 150 // Spacer to push button right
height: 1
}
Rectangle {
id: scanButton
width: Math.max(100, scanText.contentWidth + Theme.spacingL * 2)
height: 32
radius: Theme.cornerRadius
color: scanArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b, 0.08)
border.color: Theme.primary
border.width: 1
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: BluetoothService.adapter
&& BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching"
size: Theme.iconSize - 6
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: scanText
text: BluetoothService.adapter
&& BluetoothService.adapter.discovering ? "Stop Scanning" : "Scan"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: scanArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (BluetoothService.adapter)
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
}
}
}
}
Rectangle {
width: parent.width
height: noteColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08)
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g,
Theme.warning.b, 0.2)
border.width: 1
Column {
id: noteColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "info"
size: Theme.iconSize - 2
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Pairing Limitation"
font.pixelSize: Theme.fontSizeMedium
color: Theme.warning
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: "Quickshell does not support pairing devices that require pin or confirmation."
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.8)
wrapMode: Text.WordWrap
width: parent.width
}
}
}
Repeater {
model: {
if (!BluetoothService.adapter
|| !BluetoothService.adapter.discovering
|| !Bluetooth.devices)
return []
var filtered = Bluetooth.devices.values.filter(dev => {
return dev
&& !dev.paired
&& !dev.pairing
&& !dev.blocked
&& (dev.signalStrength === undefined
|| dev.signalStrength > 0)
})
return BluetoothService.sortDevices(filtered)
}
Rectangle {
property bool canConnect: BluetoothService.canConnect(modelData)
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
width: parent.width
height: 70
radius: Theme.cornerRadius
color: {
if (availableDeviceArea.containsMouse && !isBusy)
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.08)
if (modelData.pairing
|| modelData.state === BluetoothDeviceState.Connecting)
return Qt.rgba(Theme.warning.r, Theme.warning.g,
Theme.warning.b, 0.12)
if (modelData.blocked)
return Qt.rgba(Theme.error.r, Theme.error.g,
Theme.error.b, 0.08)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.08)
}
border.color: {
if (modelData.pairing)
return Theme.warning
if (modelData.blocked)
return Theme.error
return Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
}
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: BluetoothService.getDeviceIcon(modelData)
size: Theme.iconSize
color: {
if (modelData.pairing)
return Theme.warning
if (modelData.blocked)
return Theme.error
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: modelData.name || modelData.deviceName
font.pixelSize: Theme.fontSizeMedium
color: {
if (modelData.pairing)
return Theme.warning
if (modelData.blocked)
return Theme.error
return Theme.surfaceText
}
font.weight: modelData.pairing ? Font.Medium : Font.Normal
}
Row {
spacing: Theme.spacingXS
Row {
spacing: Theme.spacingS
StyledText {
text: {
if (modelData.pairing)
return "Pairing..."
if (modelData.blocked)
return "Blocked"
return BluetoothService.getSignalStrength(
modelData)
}
font.pixelSize: Theme.fontSizeSmall
color: {
if (modelData.pairing)
return Theme.warning
if (modelData.blocked)
return Theme.error
return Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
}
}
DankIcon {
name: BluetoothService.getSignalIcon(modelData)
size: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
visible: modelData.signalStrength !== undefined
&& modelData.signalStrength > 0
&& !modelData.pairing
&& !modelData.blocked
}
StyledText {
text: (modelData.signalStrength !== undefined
&& modelData.signalStrength
> 0) ? modelData.signalStrength + "%" : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.5)
visible: modelData.signalStrength !== undefined
&& modelData.signalStrength > 0
&& !modelData.pairing
&& !modelData.blocked
}
}
}
}
}
Rectangle {
width: 80
height: 28
radius: Theme.cornerRadius
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
visible: modelData.state !== BluetoothDeviceState.Connecting
color: {
if (!canConnect && !isBusy)
return Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
if (actionButtonArea.containsMouse && !isBusy)
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12)
return "transparent"
}
border.color: canConnect || isBusy ? Theme.primary : Qt.rgba(
Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
opacity: canConnect || isBusy ? 1 : 0.5
StyledText {
anchors.centerIn: parent
text: {
if (modelData.pairing)
return "Pairing..."
if (modelData.blocked)
return "Blocked"
return "Connect"
}
font.pixelSize: Theme.fontSizeSmall
color: canConnect || isBusy ? Theme.primary : Qt.rgba(
Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.5)
font.weight: Font.Medium
}
MouseArea {
id: actionButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: canConnect
&& !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (modelData)
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
MouseArea {
id: availableDeviceArea
anchors.fill: parent
anchors.rightMargin: 90
hoverEnabled: true
cursorShape: canConnect
&& !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (modelData)
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: {
if (!BluetoothService.adapter
|| !BluetoothService.adapter.discovering
|| !Bluetooth.devices)
return false
var availableCount = Bluetooth.devices.values.filter(dev => {
return dev
&& !dev.paired
&& !dev.pairing
&& !dev.blocked
&& (dev.signalStrength
=== undefined
|| dev.signalStrength > 0)
}).length
return availableCount === 0
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
DankIcon {
name: "sync"
size: Theme.iconSizeLarge
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
RotationAnimation on rotation {
running: true
loops: Animation.Infinite
from: 0
to: 360
duration: 2000
}
}
StyledText {
text: "Scanning for devices..."
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: "Make sure your device is in pairing mode"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
StyledText {
text: "No devices found. Put your device in pairing mode and click Start Scanning."
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
visible: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return true
var availableCount = Bluetooth.devices.values.filter(dev => {
return dev
&& !dev.paired
&& !dev.pairing
&& !dev.blocked
&& (dev.signalStrength
=== undefined
|| dev.signalStrength > 0)
}).length
return availableCount === 0 && !BluetoothService.adapter.discovering
}
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
}
@@ -1,363 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property var device: null
property bool modalVisible: false
property var parentItem
property var availableCodecs: []
property string currentCodec: ""
property bool isLoading: false
property bool parsingTargetCard: false
function show(bluetoothDevice) {
device = bluetoothDevice;
isLoading = true;
availableCodecs = [];
currentCodec = "";
visible = true;
modalVisible = true;
queryCodecs();
Qt.callLater(() => {
focusScope.forceActiveFocus();
});
}
function hide() {
modalVisible = false;
Qt.callLater(() => {
visible = false;
});
}
function queryCodecs() {
if (!device)
return ;
codecQueryProcess.cardName = BluetoothService.getCardName(device);
codecQueryProcess.running = true;
}
function selectCodec(profileName) {
if (!device || isLoading)
return ;
isLoading = true;
codecSwitchProcess.cardName = BluetoothService.getCardName(device);
codecSwitchProcess.profile = profileName;
codecSwitchProcess.running = true;
}
function parseCodecLine(line) {
if (!codecQueryProcess.cardName)
return ;
if (line.includes(`Name: ${codecQueryProcess.cardName}`)) {
parsingTargetCard = true;
return ;
}
if (parsingTargetCard && line.startsWith("Name: ") && !line.includes(codecQueryProcess.cardName)) {
parsingTargetCard = false;
return ;
}
if (parsingTargetCard) {
if (line.startsWith("Active Profile:")) {
let profile = line.split(": ")[1] || "";
let activeCodec = availableCodecs.find((c) => {
return c.profile === profile;
});
if (activeCodec)
currentCodec = activeCodec.name;
return ;
}
if (line.includes("codec") && line.includes("available: yes")) {
let parts = line.split(": ");
if (parts.length >= 2) {
let profile = parts[0].trim();
let description = parts[1];
let codecMatch = description.match(/codec ([^\)\s]+)/i);
let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN";
let codecInfo = BluetoothService.getCodecInfo(codecName);
if (codecInfo && !availableCodecs.some((c) => {
return c.profile === profile;
})) {
let newCodecs = availableCodecs.slice();
newCodecs.push({
"name": codecInfo.name,
"profile": profile,
"description": codecInfo.description,
"qualityColor": codecInfo.qualityColor
});
availableCodecs = newCodecs;
}
}
}
}
}
visible: false
anchors.fill: parent
color: "transparent"
z: 2000
opacity: modalVisible ? 1 : 0
FocusScope {
id: focusScope
anchors.fill: parent
focus: root.visible
enabled: root.visible
MouseArea {
anchors.fill: parent
onClicked: root.hide()
onWheel: (wheel) => {
return wheel.accepted = true;
}
}
}
Rectangle {
anchors.centerIn: parent
width: 320
height: Math.min(contentColumn.implicitHeight + Theme.spacingL * 2, 400)
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: modalVisible ? 1 : 0
scale: modalVisible ? 1 : 0.9
MouseArea {
anchors.fill: parent
onClicked: {
}
}
Column {
id: contentColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: device ? BluetoothService.getDeviceIcon(device) : "headset"
size: Theme.iconSize + 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: device ? (device.name || device.deviceName) : ""
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: "Audio Codec Selection"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
}
StyledText {
text: isLoading ? "Loading codecs..." : `Current: ${currentCodec}`
font.pixelSize: Theme.fontSizeSmall
color: isLoading ? Theme.primary : Theme.surfaceTextMedium
font.weight: Font.Medium
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: !isLoading
Repeater {
model: availableCodecs
Rectangle {
width: parent.width
height: 48
radius: Theme.cornerRadius
color: {
if (modelData.name === currentCodec)
return Theme.surfaceContainerHigh;
else if (codecMouseArea.containsMouse)
return Theme.surfaceHover;
else
return "transparent";
}
border.color: "transparent"
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Rectangle {
width: 6
height: 6
radius: 3
color: modelData.qualityColor
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: modelData.name === currentCodec ? Theme.primary : Theme.surfaceText
font.weight: modelData.name === currentCodec ? Font.Medium : Font.Normal
}
StyledText {
text: modelData.description
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
}
DankIcon {
name: "check"
size: Theme.iconSize - 4
color: Theme.primary
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
visible: modelData.name === currentCodec
}
MouseArea {
id: codecMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: modelData.name !== currentCodec && !isLoading
onClicked: {
selectCodec(modelData.profile);
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Process {
id: codecQueryProcess
property string cardName: ""
command: ["pactl", "list", "cards"]
onExited: function(exitCode, exitStatus) {
isLoading = false;
if (exitCode !== 0)
console.warn("Failed to query codecs:", exitCode);
}
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
return parseCodecLine(data.trim());
}
}
}
Process {
id: codecSwitchProcess
property string cardName: ""
property string profile: ""
command: ["pactl", "set-card-profile", cardName, profile]
onExited: function(exitCode, exitStatus) {
isLoading = false;
if (exitCode === 0) {
queryCodecs();
ToastService.showToast("Codec switched successfully", ToastService.levelInfo);
Qt.callLater(root.hide);
} else {
ToastService.showToast("Failed to switch codec", ToastService.levelError);
console.warn("Failed to switch codec:", exitCode);
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
@@ -1,265 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property var deviceData: null
property bool menuVisible: false
property var parentItem
property var codecSelector
function show(x, y) {
const menuWidth = 160;
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2;
let finalX = x - menuWidth / 2;
let finalY = y;
finalX = Math.max(0, Math.min(finalX, parentItem.width - menuWidth));
finalY = Math.max(0, Math.min(finalY, parentItem.height - menuHeight));
root.x = finalX;
root.y = finalY;
root.visible = true;
root.menuVisible = true;
}
function hide() {
root.menuVisible = false;
Qt.callLater(() => {
root.visible = false;
});
}
visible: false
width: 160
height: menuColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
z: 1000
opacity: menuVisible ? 1 : 0
scale: menuVisible ? 1 : 0.85
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: connectArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: root.deviceData && root.deviceData.connected ? "link_off" : "link"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.deviceData && root.deviceData.connected ? "Disconnect" : "Connect"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: connectArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.deviceData) {
if (root.deviceData.connected)
root.deviceData.disconnect();
else
BluetoothService.connectDeviceWithTrust(root.deviceData);
}
root.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: codecArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
visible: root.deviceData && BluetoothService.isAudioDevice(root.deviceData) && root.deviceData.connected
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "high_quality"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Audio Codec"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: codecArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
codecSelector.show(root.deviceData);
root.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: forgetArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "delete"
size: Theme.iconSize - 2
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Forget Device"
font.pixelSize: Theme.fontSizeSmall
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: forgetArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.deviceData)
root.deviceData.forget();
root.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
@@ -1,72 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
width: parent.width
height: 60
radius: Theme.cornerRadius
color: bluetoothToggle.containsMouse ? Qt.rgba(
Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12) : (BluetoothService.adapter
&& BluetoothService.adapter.enabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12))
border.color: BluetoothService.adapter
&& BluetoothService.adapter.enabled ? Theme.primary : "transparent"
border.width: 2
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "bluetooth"
size: Theme.iconSizeLarge
color: BluetoothService.adapter
&& BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Bluetooth"
font.pixelSize: Theme.fontSizeLarge
color: BluetoothService.adapter
&& BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: BluetoothService.adapter
&& BluetoothService.adapter.enabled ? "Enabled" : "Disabled"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
}
}
}
MouseArea {
id: bluetoothToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (BluetoothService.adapter)
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled
}
}
}
@@ -1,190 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: root
function findBluetoothContextMenu() {
var p = parent
while (p) {
if (p.bluetoothContextMenuWindow)
return p.bluetoothContextMenuWindow
p = p.parent
}
return null
}
width: parent.width
spacing: Theme.spacingM
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
StyledText {
text: "Paired Devices"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Repeater {
model: BluetoothService.adapter
&& BluetoothService.adapter.devices ? BluetoothService.adapter.devices.values.filter(
dev => {
return dev
&& (dev.paired
|| dev.trusted)
}) : []
Rectangle {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: btDeviceArea.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.08) : (modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData.connected ? Theme.primary : "transparent"
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: BluetoothService.getDeviceIcon(modelData)
size: Theme.iconSize
color: modelData.connected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: modelData.name || modelData.deviceName
font.pixelSize: Theme.fontSizeMedium
color: modelData.connected ? Theme.primary : Theme.surfaceText
font.weight: modelData.connected ? Font.Medium : Font.Normal
}
Row {
spacing: Theme.spacingXS
StyledText {
text: BluetoothDeviceState.toString(modelData.state)
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
}
StyledText {
text: {
if (modelData.batteryAvailable
&& modelData.battery > 0)
return "• " + Math.round(
modelData.battery * 100) + "%"
var btBattery = BatteryService.bluetoothDevices.find(
dev => {
return dev.name === (modelData.name
|| modelData.deviceName)
|| dev.name.toLowerCase(
).includes(
(modelData.name
|| modelData.deviceName).toLowerCase(
))
|| (modelData.name
|| modelData.deviceName).toLowerCase(
).includes(
dev.name.toLowerCase())
})
return btBattery ? "• " + btBattery.percentage + "%" : ""
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
}
}
Rectangle {
id: btMenuButton
width: 32
height: 32
radius: Theme.cornerRadius
color: btMenuButtonArea.containsMouse ? Qt.rgba(
Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.08) : "transparent"
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "more_vert"
size: Theme.iconSize
color: Theme.surfaceText
opacity: 0.6
anchors.centerIn: parent
}
MouseArea {
id: btMenuButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
var contextMenu = root.findBluetoothContextMenu()
if (contextMenu) {
contextMenu.deviceData = modelData
let localPos = btMenuButtonArea.mapToItem(
contextMenu.parentItem,
btMenuButtonArea.width / 2,
btMenuButtonArea.height)
contextMenu.show(localPos.x, localPos.y)
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
MouseArea {
id: btDeviceArea
anchors.fill: parent
anchors.rightMargin: 40
hoverEnabled: true
enabled: !BluetoothService.isDeviceBusy(modelData)
cursorShape: enabled ? Qt.PointingHandCursor : Qt.BusyCursor
onClicked: {
if (modelData.connected)
modelData.disconnect()
else
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
}
}
-107
View File
@@ -1,107 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Modules.ControlCenter.Bluetooth
import qs.Services
import qs.Widgets
Item {
id: bluetoothTab
property alias bluetoothContextMenuWindow: bluetoothContextMenuWindow
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingL
Loader {
width: parent.width
sourceComponent: toggleComponent
}
Loader {
width: parent.width
sourceComponent: pairedComponent
}
Loader {
width: parent.width
sourceComponent: availableComponent
}
}
}
BluetoothContextMenu {
id: bluetoothContextMenuWindow
parentItem: bluetoothTab
codecSelector: codecSelector
}
BluetoothCodecSelector {
id: codecSelector
parentItem: bluetoothTab
}
MouseArea {
anchors.fill: parent
visible: bluetoothContextMenuWindow.visible || codecSelector.visible
onClicked: {
bluetoothContextMenuWindow.hide();
codecSelector.hide();
}
MouseArea {
x: bluetoothContextMenuWindow.x
y: bluetoothContextMenuWindow.y
width: bluetoothContextMenuWindow.width
height: bluetoothContextMenuWindow.height
onClicked: {
}
}
}
Component {
id: toggleComponent
BluetoothToggle {
width: parent.width
}
}
Component {
id: pairedComponent
PairedDevicesList {
width: parent.width
}
}
Component {
id: availableComponent
AvailableDevicesList {
width: parent.width
}
}
}
@@ -1,769 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules.ControlCenter
import qs.Services
import qs.Widgets
DankPopout {
id: root
property string currentTab: "network"
property bool powerOptionsExpanded: false
property string triggerSection: "right"
property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen) {
triggerX = x
triggerY = y
triggerWidth = width
triggerSection = section
triggerScreen = screen
}
function openWithTab(tab) {
if (shouldBeVisible) {
close()
} else {
currentTab = tab
open()
}
}
signal powerActionRequested(string action, string title, string message)
signal lockRequested
popupWidth: 600
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : (powerOptionsExpanded ? 570 : 500)
triggerX: Screen.width - 600 - Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
triggerWidth: 80
positioning: "center"
WlrLayershell.namespace: "quickshell-controlcenter"
screen: triggerScreen
shouldBeVisible: false
visible: shouldBeVisible
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
NetworkService.autoRefreshEnabled = NetworkService.wifiEnabled
if (UserInfoService)
UserInfoService.getUptime()
} else {
NetworkService.autoRefreshEnabled = false
if (BluetoothService.adapter
&& BluetoothService.adapter.discovering)
BluetoothService.adapter.discovering = false
}
}
content: Component {
Rectangle {
id: controlContent
implicitHeight: {
let baseHeight = Theme.spacingL * 2
baseHeight += 90 // user header
baseHeight += (powerOptionsExpanded ? 60 : 0) + Theme.spacingL // power options
baseHeight += 52 + Theme.spacingL // tab bar
baseHeight += 280 // tab content area
return baseHeight
}
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
antialiasing: true
smooth: true
focus: true
Component.onCompleted: {
if (root.shouldBeVisible)
forceActiveFocus()
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
root.close()
event.accepted = true
} else {
event.accepted = false
}
}
Connections {
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible)
Qt.callLater(function () {
controlContent.forceActiveFocus()
})
}
target: root
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Column {
width: parent.width
spacing: Theme.spacingL
Rectangle {
width: parent.width
height: 90
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
Theme.getContentBackgroundAlpha() * 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
spacing: Theme.spacingL
Item {
id: avatarContainer
property bool hasImage: profileImageLoader.status === Image.Ready
width: 64
height: 64
Rectangle {
anchors.fill: parent
radius: width / 2
color: "transparent"
border.color: Theme.primary
border.width: 1 // The ring is 1px thick.
visible: parent.hasImage
}
Image {
id: profileImageLoader
source: {
if (PortalService.profileImage === "")
return ""
if (PortalService.profileImage.startsWith(
"/"))
return "file://" + PortalService.profileImage
return PortalService.profileImage
}
smooth: true
asynchronous: true
mipmap: true
cache: true
visible: false // This item is never shown directly.
}
MultiEffect {
anchors.fill: parent
anchors.margins: 5
source: profileImageLoader
maskEnabled: true
maskSource: circularMask
visible: avatarContainer.hasImage
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: circularMask
width: 64 - 10
height: 64 - 10
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
}
}
Rectangle {
anchors.fill: parent
radius: width / 2
color: Theme.primary
visible: !parent.hasImage
DankIcon {
anchors.centerIn: parent
name: "person"
size: Theme.iconSize + 8
color: Theme.primaryText
}
}
DankIcon {
anchors.centerIn: parent
name: "warning"
size: Theme.iconSize + 8
color: Theme.primaryText
visible: PortalService.profileImage !== ""
&& profileImageLoader.status === Image.Error
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: UserInfoService.fullName
|| UserInfoService.username || "User"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: "Uptime: " + (UserInfoService.uptime
|| "Unknown")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
font.weight: Font.Normal
}
}
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingL
spacing: Theme.spacingS
DankActionButton {
buttonSize: 40
iconName: "lock"
iconSize: Theme.iconSize - 2
iconColor: Theme.surfaceText
backgroundColor: Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.5)
hoverColor: Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b, 0.12)
onClicked: {
root.close()
root.lockRequested()
}
}
Rectangle {
width: 40
height: 40
radius: 20
color: powerButton.containsMouse
|| root.powerOptionsExpanded ? Qt.rgba(
Theme.error.r,
Theme.error.g,
Theme.error.b,
0.12) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.5)
Rectangle {
anchors.centerIn: parent
width: parent.width
height: parent.height
radius: parent.radius
color: "transparent"
clip: true
DankIcon {
id: dankIcon
anchors.centerIn: parent
name: root.powerOptionsExpanded ? "expand_less" : "power_settings_new"
size: Theme.iconSize - 2
color: powerButton.containsMouse
|| root.powerOptionsExpanded ? Theme.error : Theme.surfaceText
Behavior on name {
SequentialAnimation {
NumberAnimation {
target: dankIcon
property: "opacity"
to: 0
duration: Theme.shortDuration / 2
easing.type: Theme.standardEasing
}
PropertyAction {
target: dankIcon
property: "name"
}
NumberAnimation {
target: dankIcon
property: "opacity"
to: 1
duration: Theme.shortDuration / 2
easing.type: Theme.standardEasing
}
}
}
}
}
MouseArea {
id: powerButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
root.powerOptionsExpanded = !root.powerOptionsExpanded
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
DankActionButton {
buttonSize: 40
iconName: "settings"
iconSize: Theme.iconSize - 2
iconColor: Theme.surfaceText
backgroundColor: Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.5)
hoverColor: Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b, 0.12)
onClicked: {
root.close()
settingsModal.show()
}
}
}
}
Rectangle {
width: parent.width
height: root.powerOptionsExpanded ? 60 : 0
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
Theme.getContentBackgroundAlpha() * 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: root.powerOptionsExpanded ? 1 : 0
opacity: root.powerOptionsExpanded ? 1 : 0
clip: true
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
visible: root.powerOptionsExpanded
Rectangle {
width: 100
height: 34
radius: Theme.cornerRadius
color: logoutButton.containsMouse ? Qt.rgba(
Theme.warning.r,
Theme.warning.g,
Theme.warning.b,
0.12) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.5)
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "logout"
size: Theme.fontSizeSmall
color: logoutButton.containsMouse ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Logout"
font.pixelSize: Theme.fontSizeSmall
color: logoutButton.containsMouse ? Theme.warning : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: logoutButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
root.powerOptionsExpanded = false
root.close()
root.powerActionRequested(
"logout", "Logout",
"Are you sure you want to logout?")
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: 100
height: 34
radius: Theme.cornerRadius
color: rebootButton.containsMouse ? Qt.rgba(
Theme.warning.r,
Theme.warning.g,
Theme.warning.b,
0.12) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.5)
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "restart_alt"
size: Theme.fontSizeSmall
color: rebootButton.containsMouse ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Restart"
font.pixelSize: Theme.fontSizeSmall
color: rebootButton.containsMouse ? Theme.warning : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: rebootButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
root.powerOptionsExpanded = false
root.close()
root.powerActionRequested(
"reboot", "Restart",
"Are you sure you want to restart?")
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: 100
height: 34
radius: Theme.cornerRadius
color: suspendButton.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.5)
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "bedtime"
size: Theme.fontSizeSmall
color: suspendButton.containsMouse ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Suspend"
font.pixelSize: Theme.fontSizeSmall
color: suspendButton.containsMouse ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: suspendButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
root.powerOptionsExpanded = false
root.close()
root.powerActionRequested(
"suspend", "Suspend",
"Are you sure you want to suspend?")
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: 100
height: 34
radius: Theme.cornerRadius
color: shutdownButton.containsMouse ? Qt.rgba(
Theme.error.r,
Theme.error.g,
Theme.error.b,
0.12) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.5)
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "power_settings_new"
size: Theme.fontSizeSmall
color: shutdownButton.containsMouse ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Shutdown"
font.pixelSize: Theme.fontSizeSmall
color: shutdownButton.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: shutdownButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
root.powerOptionsExpanded = false
root.close()
root.powerActionRequested(
"poweroff", "Shutdown",
"Are you sure you want to shutdown?")
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: parent.width
height: tabBar.height + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.15)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.06)
border.width: 1
DankTabBar {
id: tabBar
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
tabHeight: 40
currentIndex: {
let tabs = ["network", "audio"]
if (BluetoothService.available)
tabs.push("bluetooth")
tabs.push("display")
return tabs.indexOf(root.currentTab)
}
model: {
let tabs = [{
"text": "Network",
"icon": "wifi",
"id": "network"
}]
tabs.push({
"text": "Audio",
"icon": "volume_up",
"id": "audio"
})
if (BluetoothService.available)
tabs.push({
"text": "Bluetooth",
"icon": "bluetooth",
"id": "bluetooth"
})
tabs.push({
"text": "Display",
"icon": "brightness_6",
"id": "display"
})
return tabs
}
onTabClicked: function (index) {
let tabs = ["network", "audio"]
if (BluetoothService.available)
tabs.push("bluetooth")
tabs.push("display")
root.currentTab = tabs[index]
}
}
}
}
Rectangle {
width: parent.width
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.1)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.05)
border.width: 1
Loader {
anchors.fill: parent
anchors.margins: Theme.spacingS
active: root.currentTab === "network"
asynchronous: true
sourceComponent: Component {
NetworkTab {}
}
}
Loader {
anchors.fill: parent
anchors.margins: Theme.spacingS
active: root.currentTab === "audio"
asynchronous: true
sourceComponent: Component {
AudioTab {}
}
}
Loader {
anchors.fill: parent
anchors.margins: Theme.spacingS
active: BluetoothService.available
&& root.currentTab === "bluetooth"
asynchronous: true
sourceComponent: Component {
BluetoothTab {}
}
}
Loader {
anchors.fill: parent
anchors.margins: Theme.spacingS
active: root.currentTab === "display"
asynchronous: true
sourceComponent: Component {
DisplayTab {}
}
}
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
-288
View File
@@ -1,288 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Modules
import qs.Services
import qs.Widgets
Item {
id: displayTab
property var brightnessDebounceTimer
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingL
Loader {
width: parent.width
sourceComponent: brightnessComponent
}
Loader {
width: parent.width
sourceComponent: settingsComponent
}
}
}
Component {
id: brightnessComponent
Column {
width: parent.width
spacing: Theme.spacingS
visible: BrightnessService.brightnessAvailable
StyledText {
text: "Brightness"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
DankDropdown {
id: deviceDropdown
width: parent.width
height: 40
visible: BrightnessService.devices.length > 1
text: "Device"
description: {
const deviceInfo = BrightnessService.getCurrentDeviceInfo()
if (deviceInfo && deviceInfo.class === "ddc") {
return "DDC changes can be slow and unreliable"
}
return ""
}
currentValue: BrightnessService.currentDevice
options: BrightnessService.devices.map(function (d) {
return d.name
})
optionIcons: BrightnessService.devices.map(function (d) {
if (d.class === "backlight")
return "desktop_windows"
if (d.class === "ddc")
return "tv"
if (d.name.includes("kbd"))
return "keyboard"
return "lightbulb"
})
onValueChanged: function (value) {
BrightnessService.setCurrentDevice(value, true)
}
Connections {
target: BrightnessService
function onDevicesChanged() {
if (BrightnessService.currentDevice) {
deviceDropdown.currentValue = BrightnessService.currentDevice
}
// Check if saved device is now available
const lastDevice = SessionData.lastBrightnessDevice
|| ""
if (lastDevice) {
const deviceExists = BrightnessService.devices.some(
d => d.name === lastDevice)
if (deviceExists
&& (!BrightnessService.currentDevice
|| BrightnessService.currentDevice !== lastDevice)) {
BrightnessService.setCurrentDevice(lastDevice,
false)
}
}
}
function onDeviceSwitched() {
// Force update the description when device switches
deviceDropdown.description = Qt.binding(function () {
const deviceInfo = BrightnessService.getCurrentDeviceInfo()
if (deviceInfo && deviceInfo.class === "ddc") {
return "DDC changes can be slow and unreliable"
}
return ""
})
}
}
}
DankSlider {
id: brightnessSlider
width: parent.width
value: BrightnessService.brightnessLevel
leftIcon: "brightness_low"
rightIcon: "brightness_high"
enabled: BrightnessService.brightnessAvailable
&& BrightnessService.isCurrentDeviceReady()
opacity: BrightnessService.isCurrentDeviceReady() ? 1.0 : 0.5
onSliderValueChanged: function (newValue) {
brightnessDebounceTimer.pendingValue = newValue
brightnessDebounceTimer.restart()
}
onSliderDragFinished: function (finalValue) {
brightnessDebounceTimer.stop()
BrightnessService.setBrightnessInternal(
finalValue, BrightnessService.currentDevice)
}
Connections {
target: BrightnessService
function onBrightnessChanged() {
brightnessSlider.value = BrightnessService.brightnessLevel
}
function onDeviceSwitched() {
brightnessSlider.value = BrightnessService.brightnessLevel
}
}
}
}
}
Component {
id: settingsComponent
Column {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Display Settings"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Row {
width: parent.width
spacing: Theme.spacingM
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadius
color: BrightnessService.nightModeActive ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : (nightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: BrightnessService.nightModeActive ? Theme.primary : "transparent"
border.width: BrightnessService.nightModeActive ? 1 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: BrightnessService.nightModeActive ? "nightlight" : "dark_mode"
size: Theme.iconSizeLarge
color: BrightnessService.nightModeActive ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "Night Mode"
font.pixelSize: Theme.fontSizeMedium
color: BrightnessService.nightModeActive ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
id: nightModeToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
BrightnessService.toggleNightMode()
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadius
color: SessionData.isLightMode ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : (lightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: SessionData.isLightMode ? Theme.primary : "transparent"
border.width: SessionData.isLightMode ? 1 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: SessionData.isLightMode ? "light_mode" : "palette"
size: Theme.iconSizeLarge
color: SessionData.isLightMode ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: SessionData.isLightMode ? "Light Mode" : "Dark Mode"
font.pixelSize: Theme.fontSizeMedium
color: SessionData.isLightMode ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
id: lightModeToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const newLightMode = !SessionData.isLightMode
SessionData.setLightMode(newLightMode)
Theme.isLightMode = newLightMode
PortalService.setLightMode(newLightMode)
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
brightnessDebounceTimer: Timer {
property int pendingValue: 0
interval: {
// Use longer interval for DDC devices since ddcutil is slow
const deviceInfo = BrightnessService.getCurrentDeviceInfo()
return (deviceInfo && deviceInfo.class === "ddc") ? 100 : 50
}
repeat: false
onTriggered: {
BrightnessService.setBrightnessInternal(
pendingValue, BrightnessService.currentDevice)
}
}
}
@@ -1,127 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: ethernetCard
width: parent.width
height: 80
radius: Theme.cornerRadius
color: {
if (ethernetPreferenceArea.containsMouse
&& NetworkService.ethernetConnected
&& NetworkService.wifiEnabled
&& NetworkService.networkStatus !== "ethernet")
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.8)
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.5)
}
border.color: NetworkService.networkStatus
=== "ethernet" ? Theme.primary : Qt.rgba(Theme.outline.r,
Theme.outline.g,
Theme.outline.b,
0.12)
border.width: NetworkService.networkStatus === "ethernet" ? 2 : 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: "lan"
size: Theme.iconSize
color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: "Ethernet"
font.pixelSize: Theme.fontSizeMedium
color: NetworkService.networkStatus
=== "ethernet" ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
}
StyledText {
text: NetworkService.ethernetConnected ? (NetworkService.ethernetIP
|| "Connected") : "Disconnected"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
elide: Text.ElideRight
}
}
}
DankIcon {
id: ethernetLoadingSpinner
name: "refresh"
size: Theme.iconSize - 4
color: Theme.primary
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.changingPreference
&& NetworkService.targetPreference === "ethernet"
z: 10
RotationAnimation {
target: ethernetLoadingSpinner
property: "rotation"
running: ethernetLoadingSpinner.visible
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
}
MouseArea {
id: ethernetPreferenceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: (NetworkService.ethernetConnected
&& NetworkService.wifiEnabled
&& NetworkService.networkStatus
!== "ethernet") ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: NetworkService.ethernetConnected && NetworkService.wifiEnabled
&& NetworkService.networkStatus !== "ethernet"
&& !NetworkService.changingNetworkPreference
onClicked: {
if (NetworkService.ethernetConnected
&& NetworkService.wifiEnabled) {
if (NetworkService.networkStatus !== "ethernet")
NetworkService.setNetworkPreference("ethernet")
else
NetworkService.setNetworkPreference("auto")
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
-170
View File
@@ -1,170 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: wifiCard
property var refreshTimer
width: parent.width
height: 80
radius: Theme.cornerRadius
color: {
if (wifiPreferenceArea.containsMouse && NetworkService.ethernetConnected
&& NetworkService.wifiEnabled
&& NetworkService.networkStatus !== "wifi")
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.8)
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.5)
}
border.color: NetworkService.networkStatus === "wifi" ? Theme.primary : Qt.rgba(
Theme.outline.r,
Theme.outline.g,
Theme.outline.b,
0.12)
border.width: NetworkService.networkStatus === "wifi" ? 2 : 1
visible: NetworkService.wifiAvailable
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
anchors.right: wifiToggle.left
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: NetworkService.wifiSignalIcon
size: Theme.iconSize
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: {
if (!NetworkService.wifiEnabled)
return "WiFi is off"
else if (NetworkService.wifiEnabled
&& NetworkService.currentWifiSSID)
return NetworkService.currentWifiSSID || "Connected"
else
return "Not Connected"
}
font.pixelSize: Theme.fontSizeMedium
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
}
StyledText {
text: {
if (!NetworkService.wifiEnabled)
return "Turn on WiFi to see networks"
else if (NetworkService.wifiEnabled
&& NetworkService.currentWifiSSID)
return NetworkService.wifiIP || "Connected"
else
return "Select a network below"
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
elide: Text.ElideRight
}
}
}
DankIcon {
id: wifiLoadingSpinner
name: "refresh"
size: Theme.iconSize - 4
color: Theme.primary
anchors.right: wifiToggle.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.changingPreference
&& NetworkService.targetPreference === "wifi"
z: 10
RotationAnimation {
target: wifiLoadingSpinner
property: "rotation"
running: wifiLoadingSpinner.visible
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
}
DankToggle {
id: wifiToggle
checked: NetworkService.wifiEnabled
enabled: true
toggling: NetworkService.wifiToggling
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
onClicked: {
if (NetworkService.wifiEnabled) {
NetworkService.currentWifiSSID = ""
NetworkService.wifiSignalStrength = 100
NetworkService.wifiNetworks = []
NetworkService.savedWifiNetworks = []
NetworkService.connectionStatus = ""
NetworkService.connectingSSID = ""
NetworkService.isScanning = false
NetworkService.refreshNetworkStatus()
}
NetworkService.toggleWifiRadio()
if (refreshTimer)
refreshTimer.triggered = true
}
}
MouseArea {
id: wifiPreferenceArea
anchors.fill: parent
anchors.rightMargin: 60 // Exclude toggle area
hoverEnabled: true
cursorShape: (NetworkService.ethernetConnected
&& NetworkService.wifiEnabled
&& NetworkService.networkStatus
!== "wifi") ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: NetworkService.ethernetConnected && NetworkService.wifiEnabled
&& NetworkService.networkStatus !== "wifi"
&& !NetworkService.changingNetworkPreference
onClicked: {
if (NetworkService.ethernetConnected
&& NetworkService.wifiEnabled) {
if (NetworkService.networkStatus !== "wifi")
NetworkService.setNetworkPreference("wifi")
else
NetworkService.setNetworkPreference("auto")
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
@@ -1,300 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: wifiContextMenuWindow
property var networkData: null
property bool menuVisible: false
property var parentItem
property var wifiPasswordModalRef
property var networkInfoModalRef
function show(x, y) {
const menuWidth = 160
wifiContextMenuWindow.visible = true
Qt.callLater(() => {
const menuHeight = wifiMenuColumn.implicitHeight + Theme.spacingS * 2
let finalX = x - menuWidth / 2
let finalY = y + 4
finalX = Math.max(
Theme.spacingS, Math.min(
finalX,
parentItem.width - menuWidth - Theme.spacingS))
finalY = Math.max(
Theme.spacingS, Math.min(
finalY,
parentItem.height - menuHeight - Theme.spacingS))
if (finalY + menuHeight > parentItem.height - Theme.spacingS) {
finalY = y - menuHeight - 4
finalY = Math.max(Theme.spacingS, finalY)
}
wifiContextMenuWindow.x = finalX
wifiContextMenuWindow.y = finalY
wifiContextMenuWindow.menuVisible = true
})
}
function hide() {
wifiContextMenuWindow.menuVisible = false
Qt.callLater(() => {
wifiContextMenuWindow.visible = false
})
}
visible: false
width: 160
height: wifiMenuColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
z: 1000
opacity: menuVisible ? 1 : 0
scale: menuVisible ? 1 : 0.85
Component.onCompleted: {
menuVisible = false
visible = false
}
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
Column {
id: wifiMenuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: connectWifiArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: wifiContextMenuWindow.networkData
&& wifiContextMenuWindow.networkData.connected ? "wifi_off" : "wifi"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: wifiContextMenuWindow.networkData
&& wifiContextMenuWindow.networkData.connected ? "Disconnect" : "Connect"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: connectWifiArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (wifiContextMenuWindow.networkData) {
if (wifiContextMenuWindow.networkData.connected) {
NetworkService.disconnectWifi()
} else {
if (wifiContextMenuWindow.networkData.saved) {
NetworkService.connectToWifi(
wifiContextMenuWindow.networkData.ssid)
} else if (wifiContextMenuWindow.networkData.secured) {
if (wifiPasswordModalRef) {
wifiPasswordModalRef.show(
wifiContextMenuWindow.networkData.ssid)
}
} else {
NetworkService.connectToWifi(
wifiContextMenuWindow.networkData.ssid)
}
}
}
wifiContextMenuWindow.hide()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: forgetWifiArea.containsMouse ? Qt.rgba(Theme.error.r,
Theme.error.g,
Theme.error.b,
0.12) : "transparent"
visible: wifiContextMenuWindow.networkData
&& (wifiContextMenuWindow.networkData.saved
|| wifiContextMenuWindow.networkData.connected)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "delete"
size: Theme.iconSize - 2
color: forgetWifiArea.containsMouse ? Theme.error : Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Forget Network"
font.pixelSize: Theme.fontSizeSmall
color: forgetWifiArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: forgetWifiArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (wifiContextMenuWindow.networkData)
NetworkService.forgetWifiNetwork(
wifiContextMenuWindow.networkData.ssid)
wifiContextMenuWindow.hide()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: infoWifiArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "info"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Network Info"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: infoWifiArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (wifiContextMenuWindow.networkData
&& networkInfoModalRef)
networkInfoModalRef.showNetworkInfo(
wifiContextMenuWindow.networkData.ssid,
wifiContextMenuWindow.networkData)
wifiContextMenuWindow.hide()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
@@ -1,310 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: root
property var wifiContextMenuWindow
property var sortedWifiNetworks
property var wifiPasswordModalRef
anchors.top: parent.top
anchors.topMargin: 100
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
visible: NetworkService.wifiEnabled
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: "Available Networks"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 170
height: 1
}
Rectangle {
width: 28
height: 28
radius: 14
color: refreshAreaSpan.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : NetworkService.isScanning ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"
DankIcon {
id: refreshIconSpan
anchors.centerIn: parent
name: "refresh"
size: Theme.iconSize - 6
color: refreshAreaSpan.containsMouse ? Theme.primary : Theme.surfaceText
rotation: NetworkService.isScanning ? refreshIconSpan.rotation : 0
RotationAnimation {
target: refreshIconSpan
property: "rotation"
running: NetworkService.isScanning
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
Behavior on rotation {
RotationAnimation {
duration: 200
easing.type: Easing.OutQuad
}
}
}
MouseArea {
id: refreshAreaSpan
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!NetworkService.isScanning) {
refreshIconSpan.rotation += 30
NetworkService.scanWifi()
}
}
}
}
}
Flickable {
width: parent.width
height: parent.height - 40
clip: true
contentWidth: width
contentHeight: spanningNetworksColumn.height
boundsBehavior: Flickable.DragAndOvershootBounds
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
flickDeceleration: 1500
maximumFlickVelocity: 2000
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => {
let delta = event.pixelDelta.y
!== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 60
let newY = parent.contentY - delta
newY = Math.max(
0, Math.min(parent.contentHeight - parent.height,
newY))
parent.contentY = newY
event.accepted = true
}
}
Column {
id: spanningNetworksColumn
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: NetworkService.wifiAvailable
&& NetworkService.wifiEnabled ? sortedWifiNetworks : []
Rectangle {
width: spanningNetworksColumn.width
height: 38
radius: Theme.cornerRadius
color: networkArea2.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.08) : modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
border.color: modelData.connected ? Theme.primary : "transparent"
border.width: modelData.connected ? 1 : 0
Item {
anchors.fill: parent
anchors.margins: Theme.spacingXS
anchors.rightMargin: Theme.spacingM // Extra right margin for scrollbar
DankIcon {
id: signalIcon2
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
name: NetworkService.wifiSignalIcon
size: Theme.iconSize - 2
color: modelData.connected ? Theme.primary : Theme.surfaceText
}
Column {
anchors.left: signalIcon2.right
anchors.leftMargin: Theme.spacingXS
anchors.right: rightIcons2.left
anchors.rightMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
width: parent.width
text: modelData.ssid
font.pixelSize: Theme.fontSizeSmall
color: modelData.connected ? Theme.primary : Theme.surfaceText
font.weight: modelData.connected ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: {
if (modelData.connected)
return "Connected"
if (NetworkService.connectionStatus === "connecting"
&& NetworkService.connectingSSID === modelData.ssid)
return "Connecting..."
if (NetworkService.connectionStatus === "invalid_password"
&& NetworkService.connectingSSID === modelData.ssid)
return "Invalid password"
if (modelData.saved)
return "Saved"
+ (modelData.secured ? " • Secured" : " • Open")
return modelData.secured ? "Secured" : "Open"
}
font.pixelSize: Theme.fontSizeSmall - 1
color: {
if (NetworkService.connectionStatus === "connecting"
&& NetworkService.connectingSSID === modelData.ssid)
return Theme.primary
if (NetworkService.connectionStatus === "invalid_password"
&& NetworkService.connectingSSID === modelData.ssid)
return Theme.error
return Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
}
elide: Text.ElideRight
}
}
Row {
id: rightIcons2
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
DankIcon {
name: "lock"
size: Theme.iconSize - 8
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.6)
visible: modelData.secured
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: wifiMenuButton
width: 24
height: 24
radius: 12
color: wifiMenuButtonArea.containsMouse ? Qt.rgba(
Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.08) : "transparent"
DankIcon {
name: "more_vert"
size: Theme.iconSize - 8
color: Theme.surfaceText
opacity: 0.6
anchors.centerIn: parent
}
MouseArea {
id: wifiMenuButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
wifiContextMenuWindow.networkData = modelData
let buttonCenter = wifiMenuButtonArea.width / 2
let buttonBottom = wifiMenuButtonArea.height
let globalPos = wifiMenuButtonArea.mapToItem(
wifiContextMenuWindow.parentItem,
buttonCenter, buttonBottom)
Qt.callLater(() => {
wifiContextMenuWindow.show(
globalPos.x,
globalPos.y)
})
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
MouseArea {
id: networkArea2
anchors.fill: parent
anchors.rightMargin: 32 // Exclude menu button area
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.connected)
return
if (modelData.saved) {
NetworkService.connectToWifi(modelData.ssid)
} else if (modelData.secured) {
if (wifiPasswordModalRef) {
wifiPasswordModalRef.show(modelData.ssid)
}
} else {
NetworkService.connectToWifi(modelData.ssid)
}
}
}
}
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
}
}
-264
View File
@@ -1,264 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Network
Item {
id: networkTab
property var wifiPasswordModalRef: {
wifiPasswordModalLoader.active = true
return wifiPasswordModalLoader.item
}
property var networkInfoModalRef: {
networkInfoModalLoader.active = true
return networkInfoModalLoader.item
}
property var sortedWifiNetworks: {
if (!NetworkService.wifiAvailable || !NetworkService.wifiEnabled) {
return []
}
var allNetworks = NetworkService.wifiNetworks
var savedNetworks = NetworkService.savedWifiNetworks
var currentSSID = NetworkService.currentWifiSSID
var signalStrength = NetworkService.wifiSignalStrengthStr
var refreshTrigger = forceRefresh
// Force recalculation
var networks = [...allNetworks]
networks.forEach(function (network) {
network.connected = (network.ssid === currentSSID)
network.saved = savedNetworks.some(function (saved) {
return saved.ssid === network.ssid
})
if (network.connected && signalStrength) {
network.signalStrength = signalStrength
}
})
networks.sort(function (a, b) {
if (a.connected && !b.connected)
return -1
if (!a.connected && b.connected)
return 1
return b.signal - a.signal
})
return networks
}
property int forceRefresh: 0
Connections {
target: NetworkService
function onNetworksUpdated() {
forceRefresh++
}
}
Component.onCompleted: {
NetworkService.addRef()
if (NetworkService.wifiEnabled)
NetworkService.scanWifi()
}
Component.onDestruction: {
NetworkService.removeRef()
}
Row {
anchors.fill: parent
spacing: Theme.spacingM
Column {
width: (parent.width - Theme.spacingM) / 2
height: parent.height
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
Flickable {
width: parent.width
height: parent.height - 30
clip: true
contentWidth: width
contentHeight: wifiContent.height
boundsBehavior: Flickable.DragAndOvershootBounds
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
flickDeceleration: 1500
maximumFlickVelocity: 2000
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => {
let delta = event.pixelDelta.y
!== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 60
let newY = parent.contentY - delta
newY = Math.max(
0, Math.min(
parent.contentHeight - parent.height,
newY))
parent.contentY = newY
event.accepted = true
}
}
Column {
id: wifiContent
width: parent.width
spacing: Theme.spacingM
WiFiCard {}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
}
}
Column {
width: (parent.width - Theme.spacingM) / 2
height: parent.height
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
Flickable {
width: parent.width
height: parent.height - 30
clip: true
contentWidth: width
contentHeight: ethernetContent.height
boundsBehavior: Flickable.StopAtBounds
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
flickDeceleration: 1500
maximumFlickVelocity: 2000
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => {
let delta = event.pixelDelta.y
!== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 60
let newY = parent.contentY - delta
newY = Math.max(
0, Math.min(
parent.contentHeight - parent.height,
newY))
parent.contentY = newY
event.accepted = true
}
}
Column {
id: ethernetContent
width: parent.width
spacing: Theme.spacingM
EthernetCard {}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
}
}
}
Rectangle {
anchors.top: parent.top
anchors.topMargin: 100
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
color: "transparent"
visible: !NetworkService.wifiEnabled
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "wifi_off"
size: 48
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: "WiFi is turned off"
font.pixelSize: Theme.fontSizeLarge
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: "Turn on WiFi to see networks"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.4)
}
}
}
WiFiNetworksList {
wifiContextMenuWindow: wifiContextMenuWindow
sortedWifiNetworks: networkTab.sortedWifiNetworks
wifiPasswordModalRef: networkTab.wifiPasswordModalRef
}
Connections {
target: NetworkService
function onWifiEnabledChanged() {
if (NetworkService.wifiEnabled && visible) {
// Trigger a scan when WiFi is enabled
NetworkService.scanWifi()
}
}
}
onVisibleChanged: {
if (visible && NetworkService.wifiEnabled
&& NetworkService.wifiNetworks.length === 0) {
// Scan when tab becomes visible if we don't have networks cached
NetworkService.scanWifi()
}
}
WiFiContextMenu {
id: wifiContextMenuWindow
parentItem: networkTab
wifiPasswordModalRef: networkTab.wifiPasswordModalRef
networkInfoModalRef: networkTab.networkInfoModalRef
}
MouseArea {
anchors.fill: parent
visible: wifiContextMenuWindow.visible
onClicked: {
wifiContextMenuWindow.hide()
}
MouseArea {
x: wifiContextMenuWindow.x
y: wifiContextMenuWindow.y
width: wifiContextMenuWindow.width
height: wifiContextMenuWindow.height
onClicked: {
}
}
}
}
-312
View File
@@ -1,312 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Widgets
PanelWindow {
id: root
property bool powerMenuVisible: false
signal powerActionRequested(string action, string title, string message)
visible: powerMenuVisible
implicitWidth: 400
implicitHeight: 320
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
MouseArea {
anchors.fill: parent
onClicked: {
powerMenuVisible = false
}
}
Rectangle {
width: Math.min(320, parent.width - Theme.spacingL * 2)
height: 320 // Fixed height to prevent cropping
x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL)
y: Theme.barHeight + Theme.spacingXS
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
opacity: powerMenuVisible ? 1 : 0
scale: powerMenuVisible ? 1 : 0.85
MouseArea {
anchors.fill: parent
onClicked: {
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
StyledText {
text: "Power Options"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 150
height: 1
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g,
Theme.error.b, 0.12)
onClicked: {
powerMenuVisible = false
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: logoutArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "logout"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Log Out"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: logoutArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerActionRequested(
"logout", "Log Out",
"Are you sure you want to log out?")
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: suspendArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "bedtime"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Suspend"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: suspendArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerActionRequested(
"suspend", "Suspend",
"Are you sure you want to suspend the system?")
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: rebootArea.containsMouse ? Qt.rgba(Theme.warning.r,
Theme.warning.g,
Theme.warning.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "restart_alt"
size: Theme.iconSize
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Reboot"
font.pixelSize: Theme.fontSizeMedium
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerActionRequested(
"reboot", "Reboot",
"Are you sure you want to reboot the system?")
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: powerOffArea.containsMouse ? Qt.rgba(Theme.error.r,
Theme.error.g,
Theme.error.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "power_settings_new"
size: Theme.iconSize
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Power Off"
font.pixelSize: Theme.fontSizeMedium
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: powerOffArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerActionRequested(
"poweroff", "Power Off",
"Are you sure you want to power off the system?")
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
-196
View File
@@ -1,196 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
id: dock
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
property var modelData
property var contextMenu
property bool autoHide: SettingsData.dockAutoHide
property real backgroundTransparency: SettingsData.dockTransparency
property bool contextMenuOpen: (contextMenu && contextMenu.visible
&& contextMenu.screen === modelData)
property bool windowIsFullscreen: {
if (!ToplevelManager.activeToplevel)
return false
var activeWindow = ToplevelManager.activeToplevel
var fullscreenApps = ["vlc", "mpv", "kodi", "steam", "lutris", "wine", "dosbox"]
return fullscreenApps.some(app => activeWindow.appId
&& activeWindow.appId.toLowerCase(
).includes(app))
}
property bool reveal: (!autoHide || dockMouseArea.containsMouse
|| dockApps.requestDockShow || contextMenuOpen)
&& !windowIsFullscreen
Connections {
target: SettingsData
function onDockTransparencyChanged() {
dock.backgroundTransparency = SettingsData.dockTransparency
}
}
screen: modelData
visible: SettingsData.showDock
color: "transparent"
anchors {
bottom: true
left: true
right: true
}
margins {
left: 0
right: 0
}
implicitHeight: 100
exclusiveZone: autoHide ? -1 : 65 - 16
mask: Region {
item: dockMouseArea
}
MouseArea {
id: dockMouseArea
height: dock.reveal ? 65 : 12
anchors {
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
implicitWidth: dock.reveal ? dockBackground.width + 32 : (dockBackground.width + 32)
hoverEnabled: true
Behavior on height {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
Item {
id: dockContainer
anchors.fill: parent
transform: Translate {
id: dockSlide
y: dock.reveal ? 0 : 60
Behavior on y {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
Rectangle {
id: dockBackground
objectName: "dockBackground"
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
width: dockApps.implicitWidth + 12
height: parent.height - 8
anchors.topMargin: 4
anchors.bottomMargin: 1
color: Qt.rgba(Theme.surfaceContainer.r,
Theme.surfaceContainer.g,
Theme.surfaceContainer.b, backgroundTransparency)
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.outlineMedium
layer.enabled: true
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g,
Theme.surfaceTint.b, 0.04)
radius: parent.radius
}
DockApps {
id: dockApps
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 4
anchors.bottomMargin: 4
contextMenu: dock.contextMenu
}
}
Rectangle {
id: appTooltip
property var hoveredButton: {
if (!dockApps.children[0])
return null
var row = dockApps.children[0]
var repeater = null
for (var i = 0; i < row.children.length; i++) {
var child = row.children[i]
if (child && typeof child.count !== "undefined"
&& typeof child.itemAt === "function") {
repeater = child
break
}
}
if (!repeater || !repeater.itemAt)
return null
for (var i = 0; i < repeater.count; i++) {
var item = repeater.itemAt(i)
if (item && item.dockButton
&& item.dockButton.showTooltip) {
return item.dockButton
}
}
return null
}
property string tooltipText: hoveredButton ? hoveredButton.tooltipText : ""
visible: hoveredButton !== null && tooltipText !== ""
width: tooltipLabel.implicitWidth + 24
height: tooltipLabel.implicitHeight + 12
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.outlineMedium
y: -height - 8
x: hoveredButton ? hoveredButton.mapToItem(
dockContainer, hoveredButton.width / 2,
0).x - width / 2 : 0
StyledText {
id: tooltipLabel
anchors.centerIn: parent
text: appTooltip.tooltipText
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
}
}
}
}
-322
View File
@@ -1,322 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var appData
property var contextMenu: null
property var dockApps: null
property int index: -1
property bool longPressing: false
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
property point dragOffset: Qt.point(0, 0)
property int targetIndex: -1
property int originalIndex: -1
property bool showWindowTitle: false
property string windowTitle: ""
property bool isHovered: mouseArea.containsMouse && !dragging
property bool showTooltip: mouseArea.containsMouse && !dragging
property string tooltipText: {
if (!appData)
return ""
// For window type, show app name + window title
if (appData.type === "window" && showWindowTitle) {
var desktopEntry = DesktopEntries.byId(appData.appId)
var appName = desktopEntry
&& desktopEntry.name ? desktopEntry.name : appData.appId
return appName + (windowTitle ? " • " + windowTitle : "")
}
// For pinned apps, just show app name
if (!appData.appId)
return ""
var desktopEntry = DesktopEntries.byId(appData.appId)
return desktopEntry
&& desktopEntry.name ? desktopEntry.name : appData.appId
}
width: 40
height: 40
onIsHoveredChanged: {
if (isHovered) {
exitAnimation.stop()
if (!bounceAnimation.running)
bounceAnimation.restart()
} else {
bounceAnimation.stop()
exitAnimation.restart()
}
}
SequentialAnimation {
id: bounceAnimation
running: false
NumberAnimation {
target: translateY
property: "y"
to: -10
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedAccel
}
NumberAnimation {
target: translateY
property: "y"
to: -8
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
}
NumberAnimation {
id: exitAnimation
running: false
target: translateY
property: "y"
to: 0
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 2
border.color: Theme.primary
visible: dragging
z: -1
}
Timer {
id: longPressTimer
interval: 500
repeat: false
onTriggered: {
if (appData && appData.isPinned)
longPressing = true
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.bottomMargin: -20
hoverEnabled: true
cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: mouse => {
if (mouse.button === Qt.LeftButton && appData
&& appData.isPinned) {
dragStartPos = Qt.point(mouse.x, mouse.y)
longPressTimer.start()
}
}
onReleased: mouse => {
longPressTimer.stop()
if (longPressing) {
if (dragging && targetIndex >= 0
&& targetIndex !== originalIndex && dockApps)
dockApps.movePinnedApp(originalIndex, targetIndex)
longPressing = false
dragging = false
dragOffset = Qt.point(0, 0)
targetIndex = -1
originalIndex = -1
}
}
onPositionChanged: mouse => {
if (longPressing && !dragging) {
var distance = Math.sqrt(
Math.pow(mouse.x - dragStartPos.x,
2) + Math.pow(
mouse.y - dragStartPos.y, 2))
if (distance > 5) {
dragging = true
targetIndex = index
originalIndex = index
}
}
if (dragging) {
dragOffset = Qt.point(
mouse.x - dragStartPos.x,
mouse.y - dragStartPos.y)
if (dockApps) {
var threshold = 40
var newTargetIndex = targetIndex
if (dragOffset.x > threshold
&& targetIndex < dockApps.pinnedAppCount - 1)
newTargetIndex = targetIndex + 1
else if (dragOffset.x < -threshold
&& targetIndex > 0)
newTargetIndex = targetIndex - 1
if (newTargetIndex !== targetIndex) {
targetIndex = newTargetIndex
dragStartPos = Qt.point(mouse.x,
mouse.y)
}
}
}
}
onClicked: mouse => {
if (!appData || longPressing)
return
if (mouse.button === Qt.LeftButton) {
// Handle based on type
if (appData.type === "pinned") {
// Launch the pinned app
if (appData && appData.appId) {
var desktopEntry = DesktopEntries.byId(
appData.appId)
if (desktopEntry)
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": desktopEntry.name
|| appData.appId,
"icon": desktopEntry.icon
|| "",
"exec": desktopEntry.exec
|| "",
"comment": desktopEntry.comment || ""
})
Quickshell.execDetached(
["gtk-launch", appData.appId])
}
} else if (appData.type === "window") {
// Focus the specific window using toplevel
if (appData.toplevelObject) {
appData.toplevelObject.activate()
}
}
} else if (mouse.button === Qt.MiddleButton) {
if (appData && appData.appId) {
var desktopEntry = DesktopEntries.byId(
appData.appId)
if (desktopEntry)
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": desktopEntry.name
|| appData.appId,
"icon": desktopEntry.icon
|| "",
"exec": desktopEntry.exec
|| "",
"comment": desktopEntry.comment
|| ""
})
Quickshell.execDetached(
["gtk-launch", appData.appId])
}
} else if (mouse.button === Qt.RightButton) {
if (contextMenu)
contextMenu.showForButton(root, appData, 40)
}
}
}
IconImage {
id: iconImg
width: 40
height: 40
anchors.centerIn: parent
source: {
if (!appData || !appData.appId)
return ""
var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry && desktopEntry.icon) {
var iconPath = Quickshell.iconPath(
desktopEntry.icon, SettingsData.iconTheme
=== "System Default" ? "" : SettingsData.iconTheme)
return iconPath
}
return ""
}
smooth: true
mipmap: true
asynchronous: true
visible: status === Image.Ready
implicitSize: 40
}
Rectangle {
width: 40
height: 40
anchors.centerIn: parent
visible: !iconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.primarySelected
Text {
anchors.centerIn: parent
text: {
if (!appData || !appData.appId)
return "?"
var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry && desktopEntry.name)
return desktopEntry.name.charAt(0).toUpperCase()
return appData.appId.charAt(0).toUpperCase()
}
font.pixelSize: 14
color: Theme.primary
font.weight: Font.Bold
}
}
// Indicator for running/focused state
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: -2
width: 8
height: 2
radius: 1
visible: appData && (appData.isRunning || appData.type === "window")
color: {
if (!appData)
return "transparent"
// For window type, check if focused using reactive property
if (appData.type === "window" && appData.toplevelObject && appData.toplevelObject.activated)
return Theme.primary
// For running apps, show dimmer indicator
if (appData.isRunning || appData.type === "window")
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.6)
return "transparent"
}
}
transform: Translate {
id: translateY
y: 0
}
}
-168
View File
@@ -1,168 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var contextMenu: null
property bool requestDockShow: false
property int pinnedAppCount: 0
implicitWidth: row.width
implicitHeight: row.height
function movePinnedApp(fromIndex, toIndex) {
if (fromIndex === toIndex)
return
var currentPinned = [...(SessionData.pinnedApps || [])]
if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0
|| toIndex >= currentPinned.length)
return
var movedApp = currentPinned.splice(fromIndex, 1)[0]
currentPinned.splice(toIndex, 0, movedApp)
SessionData.setPinnedApps(currentPinned)
}
Row {
id: row
spacing: 2
anchors.centerIn: parent
height: 40
Repeater {
id: repeater
model: ListModel {
id: dockModel
Component.onCompleted: updateModel()
function updateModel() {
clear()
var items = []
var pinnedApps = [...(SessionData.pinnedApps || [])]
// First section: Pinned apps (always visible, not representing running windows)
pinnedApps.forEach(appId => {
items.push({
"type": "pinned",
"appId": appId,
"windowId": -1,
"windowTitle"// Use -1 instead of null to avoid ListModel warnings
: "",
"workspaceId": -1,
"isPinned"// Use -1 instead of null
: true,
"isRunning": false,
})
})
root.pinnedAppCount = pinnedApps.length
// Get sorted toplevels from CompositorService
var sortedToplevels = CompositorService.sortedToplevels
// Add separator between pinned and running if both exist
if (pinnedApps.length > 0
&& sortedToplevels.length > 0) {
items.push({
"type": "separator",
"appId": "__SEPARATOR__",
"windowId": -1,
"windowTitle"// Use -1 instead of null
: "",
"workspaceId": -1,
"isPinned"// Use -1 instead of null
: false,
"isRunning": false,
"isFocused": false
})
}
// Second section: Running windows (sorted using Theme.sortToplevels)
sortedToplevels.forEach(toplevel => {
// Limit window title length for tooltip
var title = toplevel.title || "(Unnamed)"
if (title.length > 50) {
title = title.substring(0, 47) + "..."
}
items.push({
"type": "window",
"appId": toplevel.appId || "",
"windowId": -1, // Toplevel doesn't have numeric ID
"windowTitle": title,
"workspaceId": -1, // Will be handled by sorting
"isPinned": false,
"isRunning": true,
"toplevelObject": toplevel
})
})
items.forEach(item => {
append(item)
})
}
}
delegate: Item {
id: delegateItem
property alias dockButton: button
width: model.type === "separator" ? 16 : 40
height: 40
Rectangle {
visible: model.type === "separator"
width: 2
height: 20
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.3)
radius: 1
anchors.centerIn: parent
}
DockAppButton {
id: button
visible: model.type !== "separator"
anchors.centerIn: parent
width: 40
height: 40
appData: model
contextMenu: root.contextMenu
dockApps: root
index: model.index
// Override tooltip for windows to show window title
showWindowTitle: model.type === "window"
windowTitle: model.windowTitle || ""
}
}
}
}
Connections {
target: CompositorService
function onSortedToplevelsChanged() {
dockModel.updateModel()
}
}
Connections {
target: SessionData
function onPinnedAppsChanged() {
dockModel.updateModel()
}
}
}
-267
View File
@@ -1,267 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
id: root
property bool showContextMenu: false
property var appData: null
property var anchorItem: null
property real dockVisibleHeight: 40
property int margin: 10
function showForButton(button, data, dockHeight) {
anchorItem = button
appData = data
dockVisibleHeight = dockHeight || 40
var dockWindow = button.Window.window
if (dockWindow) {
for (var i = 0; i < Quickshell.screens.length; i++) {
var s = Quickshell.screens[i]
if (dockWindow.x >= s.x && dockWindow.x < s.x + s.width) {
root.screen = s
break
}
}
}
showContextMenu = true
}
function close() {
showContextMenu = false
}
screen: Quickshell.screens[0]
visible: showContextMenu
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
property point anchorPos: Qt.point(screen.width / 2, screen.height - 100)
onAnchorItemChanged: updatePosition()
onVisibleChanged: if (visible)
updatePosition()
function updatePosition() {
if (!anchorItem) {
anchorPos = Qt.point(screen.width / 2, screen.height - 100)
return
}
var dockWindow = anchorItem.Window.window
if (!dockWindow) {
anchorPos = Qt.point(screen.width / 2, screen.height - 100)
return
}
var buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0)
var actualDockHeight = root.dockVisibleHeight // fallback
function findDockBackground(item) {
if (item.objectName === "dockBackground") {
return item
}
for (var i = 0; i < item.children.length; i++) {
var found = findDockBackground(item.children[i])
if (found)
return found
}
return null
}
var dockBackground = findDockBackground(dockWindow.contentItem)
if (dockBackground) {
actualDockHeight = dockBackground.height
}
var dockBottomMargin = 16 // The dock has bottom margin
var buttonScreenY = root.screen.height - actualDockHeight - dockBottomMargin - 20
var dockContentWidth = dockWindow.width
var screenWidth = root.screen.width
var dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2)
var buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2
anchorPos = Qt.point(buttonScreenX, buttonScreenY)
}
Rectangle {
id: menuContainer
width: Math.min(400,
Math.max(200,
menuColumn.implicitWidth + Theme.spacingS * 2))
height: Math.max(60, menuColumn.implicitHeight + Theme.spacingS * 2)
x: {
var left = 10
var right = root.width - width - 10
var want = root.anchorPos.x - width / 2
return Math.max(left, Math.min(right, want))
}
y: Math.max(10, root.anchorPos.y - height + 30)
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
opacity: showContextMenu ? 1 : 0
scale: showContextMenu ? 1 : 0.85
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
Column {
id: menuColumn
width: parent.width - Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadius
color: pinArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: root.appData
&& root.appData.isPinned ? "Unpin from Dock" : "Pin to Dock"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: pinArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!root.appData)
return
if (root.appData.isPinned) {
SessionData.removePinnedApp(root.appData.appId)
} else {
SessionData.addPinnedApp(root.appData.appId)
}
root.close()
}
}
}
Rectangle {
visible: root.appData && root.appData.type === "window"
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
}
Rectangle {
visible: root.appData && root.appData.type === "window"
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
}
Rectangle {
visible: root.appData && root.appData.type === "window"
width: parent.width
height: 28
radius: Theme.cornerRadius
color: closeArea.containsMouse ? Qt.rgba(
Theme.error.r,
Theme.error.g,
Theme.error.b,
0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Close Window"
font.pixelSize: Theme.fontSizeSmall
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.appData && root.appData.toplevelObject) {
root.appData.toplevelObject.close()
}
root.close()
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
MouseArea {
anchors.fill: parent
z: -1
onClicked: {
root.close()
}
}
}
-157
View File
@@ -1,157 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Common
import qs.Services
Item {
id: root
property string sid: Quickshell.env("XDG_SESSION_ID") || "self"
property string sessionPath: ""
function activate() {
loader.activeAsync = true
}
Component.onCompleted: {
getSessionPath.running = true
}
Process {
id: getSessionPath
command: ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", "/org/freedesktop/login1", "--method", "org.freedesktop.login1.Manager.GetSession", sid]
running: false
stdout: StdioCollector {
onStreamFinished: {
const match = text.match(/objectpath '([^']+)'/)
if (match) {
root.sessionPath = match[1]
console.log("Found session path:", root.sessionPath)
checkCurrentLockState.running = true
lockStateMonitor.running = true
} else {
console.warn("Could not determine session path")
}
}
}
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
console.warn("Failed to get session path, exit code:", exitCode)
}
}
}
Process {
id: checkCurrentLockState
command: root.sessionPath ? ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", root.sessionPath, "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.login1.Session", "LockedHint"] : []
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.includes("true")) {
console.log(
"Session is locked on startup, activating lock screen")
LockScreenService.resetState()
loader.activeAsync = true
}
}
}
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
console.warn("Failed to check initial lock state, exit code:",
exitCode)
}
}
}
Process {
id: lockStateMonitor
command: root.sessionPath ? ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.login1", "--object-path", root.sessionPath] : []
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: line => {
if (line.includes("org.freedesktop.login1.Session.Lock")) {
console.log("login1: Lock signal received -> show lock")
LockScreenService.resetState()
loader.activeAsync = true
} else if (line.includes(
"org.freedesktop.login1.Session.Unlock")) {
console.log("login1: Unlock signal received -> hide lock")
loader.active = false
} else if (line.includes("LockedHint") && line.includes(
"true")) {
console.log("login1: LockedHint=true -> show lock")
LockScreenService.resetState()
loader.activeAsync = true
} else if (line.includes("LockedHint") && line.includes(
"false")) {
console.log("login1: LockedHint=false -> hide lock")
loader.active = false
}
}
}
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
console.warn("gdbus monitor failed, exit code:", exitCode)
}
}
}
LazyLoader {
id: loader
WlSessionLock {
id: sessionLock
property bool unlocked: false
property string sharedPasswordBuffer: ""
locked: true
onLockedChanged: {
if (!locked)
loader.active = false
}
LockSurface {
id: lockSurface
lock: sessionLock
sharedPasswordBuffer: sessionLock.sharedPasswordBuffer
onPasswordChanged: newPassword => {
sessionLock.sharedPasswordBuffer = newPassword
}
}
}
}
LockScreenDemo {
id: demoWindow
}
IpcHandler {
target: "lock"
function lock(): void {
console.log("Lock screen requested via IPC")
LockScreenService.resetState()
loader.activeAsync = true
}
function demo(): void {
console.log("Lock screen DEMO mode requested via IPC")
demoWindow.showDemo()
}
function isLocked(): bool {
return loader.active
}
}
}
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Modals
PanelWindow {
id: root
property bool demoActive: false
visible: demoActive
anchors {
top: true
bottom: true
left: true
right: true
}
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
color: "transparent"
function showDemo(): void {
console.log("Showing lock screen demo")
demoActive = true
}
function hideDemo(): void {
console.log("Hiding lock screen demo")
demoActive = false
}
PowerConfirmModal {
id: powerModal
}
Loader {
anchors.fill: parent
active: demoActive
sourceComponent: LockScreenContent {
demoMode: true
powerModal: powerModal
onUnlockRequested: root.hideDemo()
}
}
}
-41
View File
@@ -1,41 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Modals
WlSessionLockSurface {
id: root
required property WlSessionLock lock
required property string sharedPasswordBuffer
signal passwordChanged(string newPassword)
readonly property bool locked: lock && !lock.locked
function unlock(): void {
lock.locked = false
}
color: "transparent"
PowerConfirmModal {
id: powerConfirmModal
}
Loader {
anchors.fill: parent
sourceComponent: LockScreenContent {
demoMode: false
powerModal: powerConfirmModal
passwordBuffer: root.sharedPasswordBuffer
onUnlockRequested: root.unlock()
onPasswordBufferChanged: {
if (root.sharedPasswordBuffer !== passwordBuffer) {
root.passwordChanged(passwordBuffer)
}
}
}
}
}
@@ -1,156 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
DankListView {
id: listView
property var keyboardController: null
property bool keyboardActive: false
property bool autoScrollDisabled: false
onIsUserScrollingChanged: {
if (isUserScrolling && keyboardController
&& keyboardController.keyboardNavigationActive) {
autoScrollDisabled = true
}
}
function enableAutoScroll() {
autoScrollDisabled = false
}
property alias count: listView.count
property alias listContentHeight: listView.contentHeight
clip: true
model: NotificationService.groupedNotifications
spacing: Theme.spacingL
Timer {
id: positionPreservationTimer
interval: 200
running: keyboardController
&& keyboardController.keyboardNavigationActive
&& !autoScrollDisabled
repeat: true
onTriggered: {
if (keyboardController
&& keyboardController.keyboardNavigationActive
&& !autoScrollDisabled) {
keyboardController.ensureVisible()
}
}
}
NotificationEmptyState {
visible: listView.count === 0
anchors.centerIn: parent
}
onModelChanged: {
if (keyboardController && keyboardController.keyboardNavigationActive) {
keyboardController.rebuildFlatNavigation()
Qt.callLater(function () {
if (keyboardController
&& keyboardController.keyboardNavigationActive
&& !autoScrollDisabled) {
keyboardController.ensureVisible()
}
})
}
}
delegate: Item {
required property var modelData
required property int index
readonly property bool isExpanded: NotificationService.expandedGroups[modelData?.key]
|| false
width: ListView.view.width
height: notificationCardWrapper.height
Item {
id: notificationCardWrapper
width: parent.width
height: notificationCard.height
NotificationCard {
id: notificationCard
width: parent.width
notificationGroup: modelData
isGroupSelected: {
if (!keyboardController
|| !keyboardController.keyboardNavigationActive)
return false
keyboardController.selectionVersion
if (!listView.keyboardActive)
return false
const selection = keyboardController.getCurrentSelection()
return selection.type === "group"
&& selection.groupIndex === index
}
selectedNotificationIndex: {
if (!keyboardController
|| !keyboardController.keyboardNavigationActive)
return -1
keyboardController.selectionVersion
if (!listView.keyboardActive)
return -1
const selection = keyboardController.getCurrentSelection()
return (selection.type === "notification"
&& selection.groupIndex === index) ? selection.notificationIndex : -1
}
keyboardNavigationActive: listView.keyboardActive
}
}
}
Connections {
function onGroupedNotificationsChanged() {
if (keyboardController) {
if (keyboardController.isTogglingGroup) {
keyboardController.rebuildFlatNavigation()
return
}
keyboardController.rebuildFlatNavigation()
if (keyboardController.keyboardNavigationActive) {
Qt.callLater(function () {
if (!autoScrollDisabled) {
keyboardController.ensureVisible()
}
})
}
}
}
function onExpandedGroupsChanged() {
if (keyboardController
&& keyboardController.keyboardNavigationActive) {
Qt.callLater(function () {
if (!autoScrollDisabled) {
keyboardController.ensureVisible()
}
})
}
}
function onExpandedMessagesChanged() {
if (keyboardController
&& keyboardController.keyboardNavigationActive) {
Qt.callLater(function () {
if (!autoScrollDisabled) {
keyboardController.ensureVisible()
}
})
}
}
target: NotificationService
}
}
@@ -1,814 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property var notificationGroup
property bool expanded: NotificationService.expandedGroups[notificationGroup?.key]
|| false
property bool descriptionExpanded: NotificationService.expandedMessages[notificationGroup?.latestNotification?.notification?.id + "_desc"]
|| false
property bool userInitiatedExpansion: false
// Selection properties for keyboard navigation
property bool isGroupSelected: false
property int selectedNotificationIndex: -1
property bool keyboardNavigationActive: false
width: parent ? parent.width : 400
height: {
if (expanded) {
return expandedContent.height + 28
}
const baseHeight = 116
if (descriptionExpanded) {
return baseHeight + descriptionText.contentHeight
- (descriptionText.font.pixelSize * 1.2 * 2)
}
return baseHeight
}
radius: Theme.cornerRadius
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
color: {
// Keyboard selection highlighting for groups (both collapsed and expanded)
if (isGroupSelected && keyboardNavigationActive) {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.2)
}
// Very subtle group highlighting when navigating within expanded group
if (keyboardNavigationActive && expanded
&& selectedNotificationIndex >= 0) {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.12)
}
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.1)
}
border.color: {
// Keyboard selection highlighting for groups (both collapsed and expanded)
if (isGroupSelected && keyboardNavigationActive) {
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.5)
}
// Subtle group border when navigating within expanded group
if (keyboardNavigationActive && expanded
&& selectedNotificationIndex >= 0) {
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.2)
}
// Critical notification styling
if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) {
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.3)
}
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
}
border.width: {
// Keyboard selection highlighting for groups (both collapsed and expanded)
if (isGroupSelected && keyboardNavigationActive) {
return 1.5
}
// Subtle group border when navigating within expanded group
if (keyboardNavigationActive && expanded
&& selectedNotificationIndex >= 0) {
return 1
}
// Critical notification styling
if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) {
return 2
}
return 1
}
clip: true
Rectangle {
anchors.fill: parent
radius: parent.radius
visible: notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
position: 0.0
color: Theme.primary
}
GradientStop {
position: 0.02
color: Theme.primary
}
GradientStop {
position: 0.021
color: "transparent"
}
}
opacity: 1.0
}
Item {
id: collapsedContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 12
anchors.leftMargin: 16
anchors.rightMargin: 56
height: 92
visible: !expanded
Rectangle {
id: iconContainer
readonly property bool hasNotificationImage: notificationGroup?.latestNotification?.image && notificationGroup.latestNotification.image !== ""
width: 55
height: 55
radius: 27.5
color: Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.1)
border.color: "transparent"
border.width: 0
anchors.left: parent.left
anchors.top: parent.top
anchors.topMargin: 18
IconImage {
anchors.fill: parent
anchors.margins: 2
source: {
if (parent.hasNotificationImage)
return notificationGroup.latestNotification.cleanImage
if (notificationGroup?.latestNotification?.appIcon) {
const appIcon = notificationGroup.latestNotification.appIcon
if (appIcon.startsWith("file://") || appIcon.startsWith(
"http://") || appIcon.startsWith(
"https://"))
return appIcon
return Quickshell.iconPath(appIcon, "")
}
return ""
}
visible: status === Image.Ready
}
StyledText {
anchors.centerIn: parent
visible: !parent.hasNotificationImage
&& (!notificationGroup?.latestNotification?.appIcon
|| notificationGroup.latestNotification.appIcon === "")
text: {
const appName = notificationGroup?.appName || "?"
return appName.charAt(0).toUpperCase()
}
font.pixelSize: 20
font.weight: Font.Bold
color: Theme.primaryText
}
Rectangle {
width: 18
height: 18
radius: 9
color: Theme.primary
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: -2
anchors.rightMargin: -2
visible: (notificationGroup?.count || 0) > 1
StyledText {
anchors.centerIn: parent
text: (notificationGroup?.count
|| 0) > 99 ? "99+" : (notificationGroup?.count
|| 0).toString()
color: Theme.primaryText
font.pixelSize: 9
font.weight: Font.Bold
}
}
}
Rectangle {
id: textContainer
anchors.left: iconContainer.right
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 0
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
color: "transparent"
Item {
width: parent.width
height: parent.height
anchors.top: parent.top
anchors.topMargin: -2
Column {
width: parent.width
spacing: 2
StyledText {
width: parent.width
text: {
const timeStr = notificationGroup?.latestNotification?.timeStr
|| ""
if (timeStr.length > 0)
return (notificationGroup?.appName
|| "") + " • " + timeStr
else
return notificationGroup?.appName || ""
}
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText {
text: notificationGroup?.latestNotification?.summary
|| ""
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
maximumLineCount: 1
visible: text.length > 0
}
StyledText {
id: descriptionText
property string fullText: notificationGroup?.latestNotification?.htmlBody
|| ""
property bool hasMoreText: truncated
text: fullText
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
width: parent.width
elide: Text.ElideRight
maximumLineCount: descriptionExpanded ? -1 : 2
wrapMode: Text.WordWrap
visible: text.length > 0
linkColor: Theme.primary
onLinkActivated: link => Qt.openUrlExternally(link)
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (parent.hasMoreText || descriptionExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: mouse => {
if (!parent.hoveredLink
&& (parent.hasMoreText
|| descriptionExpanded)) {
const messageId = notificationGroup?.latestNotification?.notification?.id + "_desc"
NotificationService.toggleMessageExpansion(
messageId)
}
}
propagateComposedEvents: true
onPressed: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false
}
}
onReleased: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false
}
}
}
}
}
}
}
}
Column {
id: expandedContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 14
anchors.bottomMargin: 14
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
spacing: -1
visible: expanded
Item {
width: parent.width
height: 40
Row {
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: 56
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
StyledText {
text: notificationGroup?.appName || ""
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
maximumLineCount: 1
}
Rectangle {
width: 18
height: 18
radius: 9
color: Theme.primary
visible: (notificationGroup?.count || 0) > 1
anchors.verticalCenter: parent.verticalCenter
StyledText {
anchors.centerIn: parent
text: (notificationGroup?.count
|| 0) > 99 ? "99+" : (notificationGroup?.count
|| 0).toString()
color: Theme.primaryText
font.pixelSize: 9
font.weight: Font.Bold
}
}
}
}
Column {
width: parent.width
spacing: 16
Repeater {
model: notificationGroup?.notifications?.slice(0, 10) || []
delegate: Rectangle {
required property var modelData
required property int index
readonly property bool messageExpanded: NotificationService.expandedMessages[modelData?.notification?.id]
|| false
readonly property bool isSelected: root.selectedNotificationIndex === index
width: parent.width
height: {
const baseHeight = 120
if (messageExpanded) {
const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2
if (bodyText.implicitHeight > twoLineHeight + 2) {
const extraHeight = bodyText.implicitHeight - twoLineHeight
return baseHeight + extraHeight
}
}
return baseHeight
}
radius: Theme.cornerRadius
color: isSelected ? Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.25) : "transparent"
border.color: isSelected ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.4) : Qt.rgba(
Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.05)
border.width: isSelected ? 1 : 1
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on height {
enabled: false
}
Item {
anchors.fill: parent
anchors.margins: 12
anchors.bottomMargin: 8
Rectangle {
id: messageIcon
readonly property bool hasNotificationImage: modelData?.image
&& modelData.image !== ""
width: 32
height: 32
radius: 16
anchors.left: parent.left
anchors.top: parent.top
anchors.topMargin: 32
color: Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.1)
border.color: Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b, 0.2)
border.width: 1
IconImage {
anchors.fill: parent
anchors.margins: 1
source: {
if (parent.hasNotificationImage)
return modelData.cleanImage
if (modelData?.appIcon) {
const appIcon = modelData.appIcon
if (appIcon.startsWith("file://")
|| appIcon.startsWith("http://")
|| appIcon.startsWith(
"https://"))
return appIcon
return Quickshell.iconPath(appIcon, "")
}
return ""
}
visible: status === Image.Ready
}
StyledText {
anchors.centerIn: parent
visible: !parent.hasNotificationImage
&& (!modelData?.appIcon
|| modelData.appIcon === "")
text: {
const appName = modelData?.appName || "?"
return appName.charAt(0).toUpperCase()
}
font.pixelSize: 12
font.weight: Font.Bold
color: Theme.primaryText
}
}
Item {
anchors.left: messageIcon.right
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
anchors.top: parent.top
anchors.bottom: parent.bottom
Column {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: buttonArea.top
anchors.bottomMargin: 4
spacing: 2
StyledText {
width: parent.width
text: modelData?.timeStr || ""
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
visible: text.length > 0
}
StyledText {
width: parent.width
text: modelData?.summary || ""
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
visible: text.length > 0
}
StyledText {
id: bodyText
property bool hasMoreText: truncated
text: modelData?.htmlBody || ""
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
width: parent.width
elide: messageExpanded ? Text.ElideNone : Text.ElideRight
maximumLineCount: messageExpanded ? -1 : 2
wrapMode: Text.WordWrap
visible: text.length > 0
linkColor: Theme.primary
onLinkActivated: link => Qt.openUrlExternally(
link)
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (bodyText.hasMoreText || messageExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: mouse => {
if (!parent.hoveredLink
&& (bodyText.hasMoreText
|| messageExpanded)) {
NotificationService.toggleMessageExpansion(
modelData?.notification?.id
|| "")
}
}
propagateComposedEvents: true
onPressed: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false
}
}
onReleased: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false
}
}
}
}
}
Item {
id: buttonArea
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 30
Row {
anchors.right: parent.right
anchors.bottom: parent.bottom
spacing: 8
Repeater {
model: modelData?.actions || []
Rectangle {
property bool isHovered: false
width: Math.max(
actionText.implicitWidth + 12,
50)
height: 24
radius: 4
color: isHovered ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.1) : "transparent"
StyledText {
id: actionText
text: {
const baseText = modelData.text
|| "View"
if (keyboardNavigationActive
&& (isGroupSelected
|| selectedNotificationIndex
>= 0)) {
return `${baseText} (${index + 1})`
}
return baseText
}
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
anchors.centerIn: parent
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: parent.isHovered = true
onExited: parent.isHovered = false
onClicked: {
if (modelData
&& modelData.invoke) {
modelData.invoke()
}
}
}
}
}
Rectangle {
property bool isHovered: false
width: Math.max(
clearText.implicitWidth + 12,
50)
height: 24
radius: 4
color: isHovered ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.1) : "transparent"
StyledText {
id: clearText
text: "Clear"
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: parent.isHovered = true
onExited: parent.isHovered = false
onClicked: NotificationService.dismissNotification(
modelData)
}
}
}
}
}
}
}
}
}
}
Row {
visible: !expanded
anchors.right: clearButton.left
anchors.rightMargin: 8
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
spacing: 8
Repeater {
model: notificationGroup?.latestNotification?.actions || []
Rectangle {
property bool isHovered: false
width: Math.max(actionText.implicitWidth + 12, 50)
height: 24
radius: 4
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.1) : "transparent"
StyledText {
id: actionText
text: {
const baseText = modelData.text || "View"
if (keyboardNavigationActive && isGroupSelected) {
return `${baseText} (${index + 1})`
}
return baseText
}
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
anchors.centerIn: parent
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: parent.isHovered = true
onExited: parent.isHovered = false
onClicked: {
if (modelData && modelData.invoke) {
modelData.invoke()
}
}
}
}
}
}
Rectangle {
id: clearButton
property bool isHovered: false
visible: !expanded
anchors.right: parent.right
anchors.rightMargin: 16
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
width: clearText.width + 16
height: clearText.height + 8
radius: 6
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.1) : "transparent"
StyledText {
id: clearText
text: "Clear"
color: clearButton.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: clearButton.isHovered = true
onExited: clearButton.isHovered = false
onClicked: NotificationService.dismissGroup(
notificationGroup?.key || "")
}
}
MouseArea {
anchors.fill: parent
visible: !expanded && (notificationGroup?.count || 0) > 1
&& !descriptionExpanded
onClicked: {
root.userInitiatedExpansion = true
NotificationService.toggleGroupExpansion(
notificationGroup?.key || "")
}
z: -1
}
Item {
id: fixedControls
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: 12
anchors.rightMargin: 16
width: 60
height: 28
DankActionButton {
anchors.left: parent.left
anchors.top: parent.top
visible: (notificationGroup?.count || 0) > 1
iconName: expanded ? "expand_less" : "expand_more"
iconSize: 18
buttonSize: 28
onClicked: {
root.userInitiatedExpansion = true
NotificationService.toggleGroupExpansion(
notificationGroup?.key || "")
}
}
DankActionButton {
anchors.right: parent.right
anchors.top: parent.top
iconName: "close"
iconSize: 18
buttonSize: 28
onClicked: NotificationService.dismissGroup(
notificationGroup?.key || "")
}
}
Behavior on height {
enabled: root.userInitiatedExpansion
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
onFinished: root.userInitiatedExpansion = false
}
}
}
@@ -1,209 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Notifications.Center
DankPopout {
id: root
property bool notificationHistoryVisible: false
property string triggerSection: "right"
property var triggerScreen: null
NotificationKeyboardController {
id: keyboardController
listView: null
isOpen: notificationHistoryVisible
onClose: function () {
notificationHistoryVisible = false
}
}
function setTriggerPosition(x, y, width, section, screen) {
triggerX = x
triggerY = y
triggerWidth = width
triggerSection = section
triggerScreen = screen
}
popupWidth: 400
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 400
triggerX: Screen.width - 400 - Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
triggerWidth: 40
positioning: "center"
WlrLayershell.namespace: "quickshell-notifications"
screen: triggerScreen
shouldBeVisible: notificationHistoryVisible
visible: shouldBeVisible
onNotificationHistoryVisibleChanged: {
if (notificationHistoryVisible) {
open()
} else {
close()
}
}
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
NotificationService.onOverlayOpen()
// Set up keyboard controller when content is loaded
Qt.callLater(function () {
if (contentLoader.item) {
contentLoader.item.externalKeyboardController = keyboardController
// Find the notification list and set up the connection
var notificationList = findChild(contentLoader.item,
"notificationList")
var notificationHeader = findChild(contentLoader.item,
"notificationHeader")
if (notificationList) {
keyboardController.listView = notificationList
notificationList.keyboardController = keyboardController
}
if (notificationHeader) {
notificationHeader.keyboardController = keyboardController
}
keyboardController.reset()
keyboardController.rebuildFlatNavigation()
}
})
} else {
NotificationService.onOverlayClose()
// Reset keyboard state when closing
keyboardController.keyboardNavigationActive = false
}
}
function findChild(parent, objectName) {
if (parent.objectName === objectName) {
return parent
}
for (var i = 0; i < parent.children.length; i++) {
var child = parent.children[i]
var result = findChild(child, objectName)
if (result) {
return result
}
}
return null
}
content: Component {
Rectangle {
id: notificationContent
property var externalKeyboardController: null
property real cachedHeaderHeight: 32
implicitHeight: {
let baseHeight = Theme.spacingL * 2
baseHeight += cachedHeaderHeight
baseHeight += (notificationSettings.expanded ? notificationSettings.contentHeight : 0)
baseHeight += Theme.spacingM * 2
let listHeight = notificationList.listContentHeight
if (NotificationService.groupedNotifications.length === 0)
listHeight = 200
baseHeight += Math.min(listHeight, 600)
return Math.max(
300, Math.min(
baseHeight,
root.screen ? root.screen.height * 0.8 : Screen.height * 0.8))
}
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
focus: true
Component.onCompleted: {
if (root.shouldBeVisible)
forceActiveFocus()
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
root.close()
event.accepted = true
} else if (externalKeyboardController) {
externalKeyboardController.handleKey(event)
}
}
Connections {
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible)
Qt.callLater(function () {
notificationContent.forceActiveFocus()
})
else
notificationContent.focus = false
}
target: root
}
FocusScope {
id: contentColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
focus: true
Column {
id: contentColumnInner
anchors.fill: parent
spacing: Theme.spacingM
NotificationHeader {
id: notificationHeader
objectName: "notificationHeader"
onHeightChanged: notificationContent.cachedHeaderHeight = height
}
NotificationSettings {
id: notificationSettings
expanded: notificationHeader.showSettings
}
KeyboardNavigatedNotificationList {
id: notificationList
objectName: "notificationList"
width: parent.width
height: parent.height - notificationContent.cachedHeaderHeight
- notificationSettings.height - contentColumnInner.spacing * 2
}
}
}
NotificationKeyboardHints {
id: keyboardHints
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
showHints: externalKeyboardController ? externalKeyboardController.showKeyboardHints : false
z: 200
}
Behavior on implicitHeight {
NumberAnimation {
duration: 180
easing.type: Easing.OutQuart
}
}
}
}
}
@@ -1,36 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
width: parent.width
height: 200
visible: NotificationService.notifications.length === 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
width: parent.width * 0.8
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "notifications_none"
size: Theme.iconSizeLarge + 16
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: "Nothing to see here"
font.pixelSize: Theme.fontSizeLarge
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
}
}
}
@@ -1,169 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var keyboardController: null
property bool showSettings: false
width: parent.width
height: 32
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: "Notifications"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
DankActionButton {
id: doNotDisturbButton
iconName: SessionData.doNotDisturb ? "notifications_off" : "notifications"
iconColor: SessionData.doNotDisturb ? Theme.error : Theme.surfaceText
buttonSize: 28
anchors.verticalCenter: parent.verticalCenter
onClicked: SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
Rectangle {
id: doNotDisturbTooltip
width: tooltipText.contentWidth + Theme.spacingS * 2
height: tooltipText.contentHeight + Theme.spacingXS * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
visible: doNotDisturbButton.children[1].containsMouse // Access StateLayer's containsMouse
opacity: visible ? 1 : 0
StyledText {
id: tooltipText
text: "Do Not Disturb"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
font.hintingPreference: Font.PreferFullHinting
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
// Keyboard help button
DankActionButton {
id: helpButton
iconName: "info"
iconColor: keyboardController
&& keyboardController.showKeyboardHints ? Theme.primary : Theme.surfaceText
buttonSize: 28
visible: keyboardController !== null
anchors.verticalCenter: parent.verticalCenter
onClicked: {
if (keyboardController) {
keyboardController.showKeyboardHints = !keyboardController.showKeyboardHints
}
}
}
// Settings button
DankActionButton {
id: settingsButton
iconName: "settings"
iconColor: root.showSettings ? Theme.primary : Theme.surfaceText
buttonSize: 28
anchors.verticalCenter: parent.verticalCenter
onClicked: root.showSettings = !root.showSettings
}
Rectangle {
id: clearAllButton
width: 120
height: 28
radius: Theme.cornerRadius
visible: NotificationService.notifications.length > 0
color: clearArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: clearArea.containsMouse ? Theme.primary : Qt.rgba(
Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "delete_sweep"
size: Theme.iconSizeSmall
color: clearArea.containsMouse ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Clear All"
font.pixelSize: Theme.fontSizeSmall
color: clearArea.containsMouse ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: clearArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.clearAllNotifications()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
@@ -1,474 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
QtObject {
id: controller
property var listView: null
property bool isOpen: false
property var onClose: null
property int selectionVersion: 0
property bool keyboardNavigationActive: false
property int selectedFlatIndex: 0
property var flatNavigation: []
property bool showKeyboardHints: false
property string selectedNotificationId: ""
property string selectedGroupKey: ""
property string selectedItemType: ""
property bool isTogglingGroup: false
property bool isRebuilding: false
function rebuildFlatNavigation() {
isRebuilding = true
var nav = []
var groups = NotificationService.groupedNotifications
for (var i = 0; i < groups.length; i++) {
var group = groups[i]
var isExpanded = NotificationService.expandedGroups[group.key]
|| false
// Add the group itself
nav.push({
"type": "group",
"groupIndex": i,
"notificationIndex": -1,
"groupKey": group.key,
"notificationId": ""
})
// If expanded, add individual notifications
if (isExpanded) {
var notifications = group.notifications || []
for (var j = 0; j < notifications.length; j++) {
var notifId = String(
notifications[j]?.notification?.id || "")
nav.push({
"type": "notification",
"groupIndex": i,
"notificationIndex": j,
"groupKey": group.key,
"notificationId": notifId
})
}
}
}
flatNavigation = nav
updateSelectedIndexFromId()
isRebuilding = false
}
function updateSelectedIndexFromId() {
if (!keyboardNavigationActive)
return
// Find the index that matches our selected ID/key
for (var i = 0; i < flatNavigation.length; i++) {
var item = flatNavigation[i]
if (selectedItemType === "group" && item.type === "group"
&& item.groupKey === selectedGroupKey) {
selectedFlatIndex = i
selectionVersion++ // Trigger UI update
return
} else if (selectedItemType === "notification"
&& item.type === "notification" && String(
item.notificationId) === String(
selectedNotificationId)) {
selectedFlatIndex = i
selectionVersion++ // Trigger UI update
return
}
}
// If not found, try to find the same group but select the group header instead
if (selectedItemType === "notification") {
for (var j = 0; j < flatNavigation.length; j++) {
var groupItem = flatNavigation[j]
if (groupItem.type === "group"
&& groupItem.groupKey === selectedGroupKey) {
selectedFlatIndex = j
selectedItemType = "group"
selectedNotificationId = ""
selectionVersion++ // Trigger UI update
return
}
}
}
// If still not found, clamp to valid range and update
if (flatNavigation.length > 0) {
selectedFlatIndex = Math.min(selectedFlatIndex,
flatNavigation.length - 1)
selectedFlatIndex = Math.max(selectedFlatIndex, 0)
updateSelectedIdFromIndex()
selectionVersion++ // Trigger UI update
}
}
function updateSelectedIdFromIndex() {
if (selectedFlatIndex >= 0
&& selectedFlatIndex < flatNavigation.length) {
var item = flatNavigation[selectedFlatIndex]
selectedItemType = item.type
selectedGroupKey = item.groupKey
selectedNotificationId = item.notificationId
}
}
function reset() {
selectedFlatIndex = 0
keyboardNavigationActive = false
showKeyboardHints = false
// Reset keyboardActive when modal is reset
if (listView) {
listView.keyboardActive = false
}
rebuildFlatNavigation()
}
function selectNext() {
keyboardNavigationActive = true
if (flatNavigation.length === 0)
return
// Re-enable auto-scrolling when arrow keys are used
if (listView && listView.enableAutoScroll) {
listView.enableAutoScroll()
}
selectedFlatIndex = Math.min(selectedFlatIndex + 1,
flatNavigation.length - 1)
updateSelectedIdFromIndex()
selectionVersion++
ensureVisible()
}
function selectPrevious() {
keyboardNavigationActive = true
if (flatNavigation.length === 0)
return
// Re-enable auto-scrolling when arrow keys are used
if (listView && listView.enableAutoScroll) {
listView.enableAutoScroll()
}
selectedFlatIndex = Math.max(selectedFlatIndex - 1, 0)
updateSelectedIdFromIndex()
selectionVersion++
ensureVisible()
}
function toggleGroupExpanded() {
if (flatNavigation.length === 0
|| selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex]
if (!group)
return
// Prevent expanding groups with < 2 notifications
const notificationCount = group.notifications ? group.notifications.length : 0
if (notificationCount < 2)
return
const wasExpanded = NotificationService.expandedGroups[group.key]
|| false
const groupIndex = currentItem.groupIndex
isTogglingGroup = true
NotificationService.toggleGroupExpansion(group.key)
rebuildFlatNavigation()
// Smart selection after toggle
if (!wasExpanded) {
// Just expanded - move to first notification in the group
for (var i = 0; i < flatNavigation.length; i++) {
if (flatNavigation[i].type === "notification"
&& flatNavigation[i].groupIndex === groupIndex) {
selectedFlatIndex = i
break
}
}
} else {
// Just collapsed - stay on the group header
for (var i = 0; i < flatNavigation.length; i++) {
if (flatNavigation[i].type === "group"
&& flatNavigation[i].groupIndex === groupIndex) {
selectedFlatIndex = i
break
}
}
}
isTogglingGroup = false
ensureVisible()
}
function handleEnterKey() {
if (flatNavigation.length === 0
|| selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex]
if (!group)
return
if (currentItem.type === "group") {
const notificationCount = group.notifications ? group.notifications.length : 0
if (notificationCount >= 2) {
toggleGroupExpanded()
} else {
executeAction(0)
}
} else if (currentItem.type === "notification") {
executeAction(0)
}
}
function toggleTextExpanded() {
if (flatNavigation.length === 0
|| selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex]
if (!group)
return
var messageId = ""
if (currentItem.type === "group") {
messageId = group.latestNotification?.notification?.id + "_desc"
} else if (currentItem.type === "notification"
&& currentItem.notificationIndex >= 0
&& currentItem.notificationIndex < group.notifications.length) {
messageId = group.notifications[currentItem.notificationIndex]?.notification?.id
+ "_desc"
}
if (messageId) {
NotificationService.toggleMessageExpansion(messageId)
}
}
function executeAction(actionIndex) {
if (flatNavigation.length === 0
|| selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex]
if (!group)
return
var actions = []
if (currentItem.type === "group") {
actions = group.latestNotification?.actions || []
} else if (currentItem.type === "notification"
&& currentItem.notificationIndex >= 0
&& currentItem.notificationIndex < group.notifications.length) {
actions = group.notifications[currentItem.notificationIndex]?.actions
|| []
}
if (actionIndex >= 0 && actionIndex < actions.length) {
const action = actions[actionIndex]
if (action.invoke) {
action.invoke()
if (onClose)
onClose()
}
}
}
function clearSelected() {
if (flatNavigation.length === 0
|| selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex]
if (!group)
return
if (currentItem.type === "group") {
NotificationService.dismissGroup(group.key)
} else if (currentItem.type === "notification") {
const notification = group.notifications[currentItem.notificationIndex]
NotificationService.dismissNotification(notification)
}
rebuildFlatNavigation()
if (flatNavigation.length === 0) {
keyboardNavigationActive = false
if (listView) {
listView.keyboardActive = false
}
} else {
selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1)
updateSelectedIdFromIndex()
ensureVisible()
}
}
function ensureVisible() {
if (flatNavigation.length === 0
|| selectedFlatIndex >= flatNavigation.length || !listView)
return
const currentItem = flatNavigation[selectedFlatIndex]
if (keyboardNavigationActive && currentItem
&& currentItem.groupIndex >= 0) {
// Always center the selected item for better visibility
// This ensures the selected item stays in view even when new notifications arrive
if (currentItem.type === "notification") {
// For individual notifications, center on the group but bias towards the notification
listView.positionViewAtIndex(currentItem.groupIndex,
ListView.Center)
} else {
// For group headers, center on the group
listView.positionViewAtIndex(currentItem.groupIndex,
ListView.Center)
}
// Force immediate update
listView.forceLayout()
}
}
function handleKey(event) {
if ((event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace)
&& (event.modifiers & Qt.ShiftModifier)) {
NotificationService.clearAllNotifications()
rebuildFlatNavigation()
if (flatNavigation.length === 0) {
keyboardNavigationActive = false
if (listView) {
listView.keyboardActive = false
}
} else {
selectedFlatIndex = 0
updateSelectedIdFromIndex()
}
selectionVersion++
event.accepted = true
return
}
if (event.key === Qt.Key_Escape) {
if (keyboardNavigationActive) {
keyboardNavigationActive = false
event.accepted = true
} else {
if (onClose)
onClose()
event.accepted = true
}
} else if (event.key === Qt.Key_Down || event.key === 16777237) {
if (!keyboardNavigationActive) {
keyboardNavigationActive = true
rebuildFlatNavigation() // Ensure we have fresh navigation data
selectedFlatIndex = 0
updateSelectedIdFromIndex()
// Set keyboardActive on listView to show highlight
if (listView) {
listView.keyboardActive = true
}
selectionVersion++
ensureVisible()
event.accepted = true
} else {
selectNext()
event.accepted = true
}
} else if (event.key === Qt.Key_Up || event.key === 16777235) {
if (!keyboardNavigationActive) {
keyboardNavigationActive = true
rebuildFlatNavigation() // Ensure we have fresh navigation data
selectedFlatIndex = 0
updateSelectedIdFromIndex()
// Set keyboardActive on listView to show highlight
if (listView) {
listView.keyboardActive = true
}
selectionVersion++
ensureVisible()
event.accepted = true
} else if (selectedFlatIndex === 0) {
keyboardNavigationActive = false
// Reset keyboardActive when navigation is disabled
if (listView) {
listView.keyboardActive = false
}
selectionVersion++
event.accepted = true
return
} else {
selectPrevious()
event.accepted = true
}
} else if (keyboardNavigationActive) {
if (event.key === Qt.Key_Space) {
toggleGroupExpanded()
event.accepted = true
} else if (event.key === Qt.Key_Return
|| event.key === Qt.Key_Enter) {
handleEnterKey()
event.accepted = true
} else if (event.key === Qt.Key_E) {
toggleTextExpanded()
event.accepted = true
} else if (event.key === Qt.Key_Delete
|| event.key === Qt.Key_Backspace) {
clearSelected()
event.accepted = true
} else if (event.key >= Qt.Key_1 && event.key <= Qt.Key_9) {
const actionIndex = event.key - Qt.Key_1
executeAction(actionIndex)
event.accepted = true
}
}
if (event.key === Qt.Key_F10) {
showKeyboardHints = !showKeyboardHints
event.accepted = true
}
}
// Get current selection info for UI
function getCurrentSelection() {
if (!keyboardNavigationActive || selectedFlatIndex < 0
|| selectedFlatIndex >= flatNavigation.length) {
return {
"type": "",
"groupIndex": -1,
"notificationIndex": -1
}
}
const result = flatNavigation[selectedFlatIndex] || {
"type": "",
"groupIndex": -1,
"notificationIndex": -1
}
return result
}
}
@@ -1,51 +0,0 @@
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: root
property bool showHints: false
height: 80
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.95)
border.color: Theme.primary
border.width: 2
opacity: showHints ? 1 : 0
z: 100
Column {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingS
spacing: 2
StyledText {
text: "↑/↓: Nav • Space: Expand • Enter: Action/Expand • E: Text"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: "Del: Clear • Shift+Del: Clear All • 1-9: Actions • F10: Help • Esc: Close"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
@@ -1,263 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool expanded: false
readonly property real contentHeight: contentColumn.height + Theme.spacingL * 2
width: parent.width
height: expanded ? Math.min(contentHeight, 400) : 0
visible: expanded
clip: true
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.1)
border.width: 1
Behavior on height {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
// Ensure smooth opacity transition
opacity: expanded ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
readonly property var timeoutOptions: [{
"text": "Never",
"value": 0
}, {
"text": "1 second",
"value": 1000
}, {
"text": "3 seconds",
"value": 3000
}, {
"text": "5 seconds",
"value": 5000
}, {
"text": "8 seconds",
"value": 8000
}, {
"text": "10 seconds",
"value": 10000
}, {
"text": "15 seconds",
"value": 15000
}, {
"text": "30 seconds",
"value": 30000
}, {
"text": "1 minute",
"value": 60000
}, {
"text": "2 minutes",
"value": 120000
}, {
"text": "5 minutes",
"value": 300000
}, {
"text": "10 minutes",
"value": 600000
}]
function getTimeoutText(value) {
if (value === undefined || value === null || isNaN(value)) {
return "5 seconds"
}
for (var i = 0; i < timeoutOptions.length; i++) {
if (timeoutOptions[i].value === value) {
return timeoutOptions[i].text
}
}
if (value === 0)
return "Never"
if (value < 1000)
return value + "ms"
if (value < 60000)
return Math.round(value / 1000) + " seconds"
return Math.round(value / 60000) + " minutes"
}
Column {
id: contentColumn
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
StyledText {
text: "Notification Settings"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
}
Item {
width: parent.width
height: 36
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: SessionData.doNotDisturb ? "notifications_off" : "notifications"
size: Theme.iconSizeSmall
color: SessionData.doNotDisturb ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Do Not Disturb"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
checked: SessionData.doNotDisturb
onToggled: SessionData.setDoNotDisturb(
!SessionData.doNotDisturb)
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.1)
}
StyledText {
text: "Notification Timeouts"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
}
DankDropdown {
width: parent.width
text: "Low Priority"
description: "Timeout for low priority notifications"
currentValue: getTimeoutText(SettingsData.notificationTimeoutLow)
options: timeoutOptions.map(opt => opt.text)
onValueChanged: value => {
for (var i = 0; i < timeoutOptions.length; i++) {
if (timeoutOptions[i].text === value) {
SettingsData.setNotificationTimeoutLow(
timeoutOptions[i].value)
break
}
}
}
}
DankDropdown {
width: parent.width
text: "Normal Priority"
description: "Timeout for normal priority notifications"
currentValue: getTimeoutText(SettingsData.notificationTimeoutNormal)
options: timeoutOptions.map(opt => opt.text)
onValueChanged: value => {
for (var i = 0; i < timeoutOptions.length; i++) {
if (timeoutOptions[i].text === value) {
SettingsData.setNotificationTimeoutNormal(
timeoutOptions[i].value)
break
}
}
}
}
DankDropdown {
width: parent.width
text: "Critical Priority"
description: "Timeout for critical priority notifications"
currentValue: getTimeoutText(
SettingsData.notificationTimeoutCritical)
options: timeoutOptions.map(opt => opt.text)
onValueChanged: value => {
for (var i = 0; i < timeoutOptions.length; i++) {
if (timeoutOptions[i].text === value) {
SettingsData.setNotificationTimeoutCritical(
timeoutOptions[i].value)
break
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.1)
}
Item {
width: parent.width
height: 36
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "notifications_active"
size: Theme.iconSizeSmall
color: SettingsData.notificationOverlayEnabled ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Notification Overlay"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
StyledText {
text: "Display all priorities over fullscreen apps"
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
}
}
}
DankToggle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.notificationOverlayEnabled
onToggled: toggled => SettingsData.setNotificationOverlayEnabled(
toggled)
}
}
}
}
@@ -1,627 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import Quickshell.Services.Notifications
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
id: win
required property var notificationData
required property string notificationId
readonly property bool hasValidData: notificationData
&& notificationData.notification
property int screenY: 0
property bool exiting: false
property bool _isDestroying: false
property bool _finalized: false
signal entered
signal exitFinished
function startExit() {
if (exiting || _isDestroying)
return
exiting = true
exitAnim.restart()
exitWatchdog.restart()
if (NotificationService.removeFromVisibleNotifications)
NotificationService.removeFromVisibleNotifications(
win.notificationData)
}
function forceExit() {
if (_isDestroying)
return
_isDestroying = true
exiting = true
visible = false
exitWatchdog.stop()
finalizeExit("forced")
}
function finalizeExit(reason) {
if (_finalized)
return
_finalized = true
_isDestroying = true
exitWatchdog.stop()
wrapperConn.enabled = false
wrapperConn.target = null
win.exitFinished()
}
visible: hasValidData
WlrLayershell.layer: {
if (!notificationData)
return WlrLayershell.Top
SettingsData.notificationOverlayEnabled
const shouldUseOverlay = (SettingsData.notificationOverlayEnabled)
|| (notificationData.urgency === NotificationUrgency.Critical)
return shouldUseOverlay ? WlrLayershell.Overlay : WlrLayershell.Top
}
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
implicitWidth: 400
implicitHeight: 122
onScreenYChanged: margins.top = Theme.barHeight - 4 + SettingsData.topBarSpacing + 4 + screenY
onHasValidDataChanged: {
if (!hasValidData && !exiting && !_isDestroying) {
forceExit()
}
}
Component.onCompleted: {
if (hasValidData) {
Qt.callLater(() => {
return enterX.restart()
})
} else {
forceExit()
}
}
onNotificationDataChanged: {
if (!_isDestroying) {
wrapperConn.target = win.notificationData || null
notificationConn.target = (win.notificationData
&& win.notificationData.notification
&& win.notificationData.notification.Retainable)
|| null
}
}
onEntered: {
if (!_isDestroying)
enterDelay.start()
}
Component.onDestruction: {
_isDestroying = true
exitWatchdog.stop()
if (notificationData && notificationData.timer)
notificationData.timer.stop()
}
anchors {
top: true
right: true
}
margins {
top: Theme.barHeight - 4 + SettingsData.topBarSpacing + 4
right: 12
}
Item {
id: content
anchors.fill: parent
visible: win.hasValidData
layer.enabled: (enterX.running || exitAnim.running)
layer.smooth: true
Rectangle {
property var shadowLayers: [shadowLayer1, shadowLayer2, shadowLayer3]
anchors.fill: parent
anchors.margins: 4
radius: Theme.cornerRadius
color: Theme.popupBackground()
border.color: notificationData && notificationData.urgency
=== NotificationUrgency.Critical ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.3) : Qt.rgba(
Theme.outline.r,
Theme.outline.g,
Theme.outline.b,
0.08)
border.width: notificationData
&& notificationData.urgency === NotificationUrgency.Critical ? 2 : 1
clip: true
Rectangle {
id: shadowLayer1
anchors.fill: parent
anchors.margins: -3
color: "transparent"
radius: parent.radius + 3
border.color: Qt.rgba(0, 0, 0, 0.05)
border.width: 1
z: -3
}
Rectangle {
id: shadowLayer2
anchors.fill: parent
anchors.margins: -2
color: "transparent"
radius: parent.radius + 2
border.color: Qt.rgba(0, 0, 0, 0.08)
border.width: 1
z: -2
}
Rectangle {
id: shadowLayer3
anchors.fill: parent
color: "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.12)
border.width: 1
radius: parent.radius
z: -1
}
Rectangle {
anchors.fill: parent
radius: parent.radius
visible: notificationData
&& notificationData.urgency === NotificationUrgency.Critical
opacity: 1
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
position: 0
color: Theme.primary
}
GradientStop {
position: 0.02
color: Theme.primary
}
GradientStop {
position: 0.021
color: "transparent"
}
}
}
Item {
id: notificationContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 12
anchors.leftMargin: 16
anchors.rightMargin: 56
height: 98
Rectangle {
id: iconContainer
readonly property bool hasNotificationImage: notificationData
&& notificationData.image
&& notificationData.image !== ""
width: 55
height: 55
radius: 27.5
color: Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.1)
border.color: "transparent"
border.width: 0
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
IconImage {
id: iconImage
anchors.fill: parent
anchors.margins: 2
asynchronous: true
source: {
if (!notificationData)
return ""
if (parent.hasNotificationImage)
return notificationData.cleanImage || ""
if (notificationData.appIcon) {
const appIcon = notificationData.appIcon
if (appIcon.startsWith("file://")
|| appIcon.startsWith("http://")
|| appIcon.startsWith("https://"))
return appIcon
return Quickshell.iconPath(appIcon, "")
}
return ""
}
visible: status === Image.Ready
}
StyledText {
anchors.centerIn: parent
visible: !parent.hasNotificationImage
&& (!notificationData
|| !notificationData.appIcon
|| notificationData.appIcon === "")
text: {
const appName = notificationData
&& notificationData.appName ? notificationData.appName : "?"
return appName.charAt(0).toUpperCase()
}
font.pixelSize: 20
font.weight: Font.Bold
color: Theme.primaryText
}
}
Rectangle {
id: textContainer
anchors.left: iconContainer.right
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 0
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
color: "transparent"
Item {
width: parent.width
height: parent.height
anchors.top: parent.top
anchors.topMargin: -2
Column {
width: parent.width
spacing: 2
StyledText {
width: parent.width
text: {
if (!notificationData)
return ""
const appName = notificationData.appName
|| ""
const timeStr = notificationData.timeStr
|| ""
if (timeStr.length > 0)
return appName + " • " + timeStr
else
return appName
}
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText {
text: notificationData ? (notificationData.summary
|| "") : ""
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
maximumLineCount: 1
visible: text.length > 0
}
StyledText {
text: notificationData ? (notificationData.htmlBody
|| "") : ""
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
width: parent.width
elide: Text.ElideRight
maximumLineCount: 2
wrapMode: Text.WordWrap
visible: text.length > 0
linkColor: Theme.primary
onLinkActivated: link => {
return Qt.openUrlExternally(
link)
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
}
}
}
}
DankActionButton {
id: closeButton
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: 12
anchors.rightMargin: 16
iconName: "close"
iconSize: 18
buttonSize: 28
z: 15
onClicked: {
if (notificationData && !win.exiting)
notificationData.popup = false
}
}
Row {
anchors.right: clearButton.left
anchors.rightMargin: 8
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
spacing: 8
z: 20
Repeater {
model: notificationData ? (notificationData.actions
|| []) : []
Rectangle {
property bool isHovered: false
width: Math.max(actionText.implicitWidth + 12, 50)
height: 24
radius: 4
color: isHovered ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.1) : "transparent"
StyledText {
id: actionText
text: modelData.text || "View"
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
anchors.centerIn: parent
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onEntered: parent.isHovered = true
onExited: parent.isHovered = false
onClicked: {
if (modelData && modelData.invoke)
modelData.invoke()
if (notificationData && !win.exiting)
notificationData.popup = false
}
}
}
}
}
Rectangle {
id: clearButton
property bool isHovered: false
anchors.right: parent.right
anchors.rightMargin: 16
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
width: Math.max(clearText.implicitWidth + 12, 50)
height: 24
radius: 4
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.1) : "transparent"
z: 20
StyledText {
id: clearText
text: "Clear"
color: clearButton.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onEntered: clearButton.isHovered = true
onExited: clearButton.isHovered = false
onClicked: {
if (notificationData && !win.exiting)
NotificationService.dismissNotification(
notificationData)
}
}
}
MouseArea {
id: cardHoverArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
propagateComposedEvents: true
z: -1
onEntered: {
if (notificationData && notificationData.timer)
notificationData.timer.stop()
}
onExited: {
if (notificationData && notificationData.popup
&& notificationData.timer)
notificationData.timer.restart()
}
onClicked: {
if (notificationData && !win.exiting)
notificationData.popup = false
}
}
}
transform: Translate {
id: tx
x: Anims.slidePx
}
}
NumberAnimation {
id: enterX
target: tx
property: "x"
from: Anims.slidePx
to: 0
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
onStopped: {
if (!win.exiting && !win._isDestroying && Math.abs(tx.x) < 0.5) {
win.entered()
}
}
}
ParallelAnimation {
id: exitAnim
onStopped: finalizeExit("animStopped")
PropertyAnimation {
target: tx
property: "x"
from: 0
to: Anims.slidePx
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedAccel
}
NumberAnimation {
target: content
property: "opacity"
from: 1
to: 0
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardAccel
}
NumberAnimation {
target: content
property: "scale"
from: 1
to: 0.98
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedAccel
}
}
Connections {
id: wrapperConn
function onPopupChanged() {
if (!win.notificationData || win._isDestroying)
return
if (!win.notificationData.popup && !win.exiting)
startExit()
}
target: win.notificationData || null
ignoreUnknownSignals: true
enabled: !win._isDestroying
}
Connections {
id: notificationConn
function onDropped() {
if (!win._isDestroying && !win.exiting)
forceExit()
}
target: (win.notificationData && win.notificationData.notification
&& win.notificationData.notification.Retainable) || null
ignoreUnknownSignals: true
enabled: !win._isDestroying
}
Timer {
id: enterDelay
interval: 160
repeat: false
onTriggered: {
if (notificationData && notificationData.timer && !exiting
&& !_isDestroying)
notificationData.timer.start()
}
}
Timer {
id: exitWatchdog
interval: 600
repeat: false
onTriggered: finalizeExit("watchdog")
}
Behavior on screenY {
id: screenYAnim
enabled: !exiting && !_isDestroying
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardDecel
}
}
}
@@ -1,286 +0,0 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
QtObject {
id: manager
property var modelData
property int topMargin: 0
property int baseNotificationHeight: 120
property int maxTargetNotifications: 4
property var popupWindows: [] // strong refs to windows (live until exitFinished)
property var destroyingWindows: new Set()
property Component popupComponent
popupComponent: Component {
NotificationPopup {
onEntered: manager._onPopupEntered(this)
onExitFinished: manager._onPopupExitFinished(this)
}
}
property Connections notificationConnections
notificationConnections: Connections {
function onVisibleNotificationsChanged() {
manager._sync(NotificationService.visibleNotifications)
}
target: NotificationService
}
property Timer sweeper
sweeper: Timer {
interval: 500
running: false
repeat: true
onTriggered: {
let toRemove = []
for (let p of popupWindows) {
if (!p) { toRemove.push(p); continue }
const isZombie =
p.status === Component.Null ||
(!p.visible && !p.exiting) ||
(!p.notificationData && !p._isDestroying) ||
(!p.hasValidData && !p._isDestroying)
if (isZombie) {
toRemove.push(p)
if (p.forceExit) p.forceExit()
else if (p.destroy) { try { p.destroy() } catch(e) {} }
}
}
if (toRemove.length) {
popupWindows = popupWindows.filter(p => toRemove.indexOf(p) === -1)
const survivors = _active().sort((a,b)=>a.screenY-b.screenY)
for (var k=0; k<survivors.length; ++k)
survivors[k].screenY = topMargin + k * baseNotificationHeight
}
if (popupWindows.length === 0) sweeper.stop()
}
}
function _hasWindowFor(w) {
return popupWindows.some(p => {
return p && p.notificationData === w
&& !p._isDestroying
&& p.status !== Component.Null
})
}
function _isValidWindow(p) {
return p && p.status !== Component.Null && !p._isDestroying
&& p.hasValidData
}
function _canMakeRoomFor(wrapper) {
const activeWindows = _active()
if (activeWindows.length < maxTargetNotifications)
return true
if (!wrapper || !wrapper.notification)
return false
const incomingUrgency = wrapper.notification.urgency || 0
for (let p of activeWindows) {
if (!p.notificationData || !p.notificationData.notification)
continue
const existingUrgency = p.notificationData.notification.urgency || 0
if (existingUrgency < incomingUrgency)
return true
if (existingUrgency === incomingUrgency) {
const timer = p.notificationData.timer
if (timer && !timer.running)
return true
}
}
return false
}
function _makeRoomForNew(wrapper) {
const activeWindows = _active()
if (activeWindows.length < maxTargetNotifications)
return
const toRemove = _selectPopupToRemove(activeWindows, wrapper)
if (toRemove && !toRemove.exiting) {
toRemove.notificationData.removedByLimit = true
toRemove.notificationData.popup = false
if (toRemove.notificationData.timer)
toRemove.notificationData.timer.stop()
}
}
function _selectPopupToRemove(activeWindows, incomingWrapper) {
const incomingUrgency = (incomingWrapper && incomingWrapper.notification)
? incomingWrapper.notification.urgency || 0 : 0
const sortedWindows = activeWindows.slice().sort((a, b) => {
const aUrgency = (a.notificationData && a.notificationData.notification)
? a.notificationData.notification.urgency || 0 : 0
const bUrgency = (b.notificationData && b.notificationData.notification)
? b.notificationData.notification.urgency || 0 : 0
if (aUrgency !== bUrgency)
return aUrgency - bUrgency
const aTimer = a.notificationData && a.notificationData.timer
const bTimer = b.notificationData && b.notificationData.timer
const aRunning = aTimer && aTimer.running
const bRunning = bTimer && bTimer.running
if (aRunning !== bRunning)
return aRunning ? 1 : -1
return b.screenY - a.screenY
})
return sortedWindows[0]
}
function _sync(newWrappers) {
for (let w of newWrappers) {
if (w && !_hasWindowFor(w))
insertNewestAtTop(w)
}
for (let p of popupWindows.slice()) {
if (!_isValidWindow(p))
continue
if (p.notificationData && newWrappers.indexOf(
p.notificationData) === -1 && !p.exiting) {
p.notificationData.removedByLimit = true
p.notificationData.popup = false
}
}
}
function insertNewestAtTop(wrapper) {
if (!wrapper) {
return
}
for (let p of popupWindows) {
if (!_isValidWindow(p))
continue
if (p.exiting)
continue
p.screenY = p.screenY + baseNotificationHeight
}
const notificationId = wrapper
&& wrapper.notification ? wrapper.notification.id : ""
const win = popupComponent.createObject(null, {
"notificationData": wrapper,
"notificationId": notificationId,
"screenY": topMargin,
"screen": manager.modelData
})
if (!win) {
return
}
if (!win.hasValidData) {
win.destroy()
return
}
popupWindows.push(win)
if (!sweeper.running)
sweeper.start()
}
function _active() {
return popupWindows.filter(p => {
return _isValidWindow(p)
&& p.notificationData
&& p.notificationData.popup && !p.exiting
})
}
function _bottom() {
let b = null, maxY = -1
for (let p of _active()) {
if (p.screenY > maxY) {
maxY = p.screenY
b = p
}
}
return b
}
function _onPopupEntered(p) {
}
function _onPopupExitFinished(p) {
if (!p)
return
const windowId = p.toString()
if (destroyingWindows.has(windowId))
return
destroyingWindows.add(windowId)
const i = popupWindows.indexOf(p)
if (i !== -1) {
popupWindows.splice(i, 1)
popupWindows = popupWindows.slice()
}
if (NotificationService.releaseWrapper && p.notificationData)
NotificationService.releaseWrapper(p.notificationData)
Qt.callLater(() => {
if (p && p.destroy) {
try {
p.destroy()
} catch (e) {
}
}
Qt.callLater(() => {
destroyingWindows.delete(windowId)
})
})
const survivors = _active().sort((a, b) => {
return a.screenY - b.screenY
})
for (var k = 0; k < survivors.length; ++k) {
survivors[k].screenY = topMargin + k * baseNotificationHeight
}
}
function cleanupAllWindows() {
sweeper.stop()
for (let p of popupWindows.slice()) {
if (p) {
try {
if (p.forceExit)
p.forceExit()
else if (p.destroy)
p.destroy()
} catch (e) {
}
}
}
popupWindows = []
destroyingWindows.clear()
}
onPopupWindowsChanged: {
if (popupWindows.length > 0 && !sweeper.running)
sweeper.start()
else if (popupWindows.length === 0 && sweeper.running)
sweeper.stop()
}
}
-130
View File
@@ -1,130 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
DankOSD {
id: root
osdWidth: Math.min(260, Screen.width - Theme.spacingM * 2)
osdHeight: 40 + Theme.spacingS * 2
autoHideInterval: 3000
enableMouseInteraction: true
property var brightnessDebounceTimer: Timer {
property int pendingValue: 0
interval: {
const deviceInfo = BrightnessService.getCurrentDeviceInfo()
return (deviceInfo && deviceInfo.class === "ddc") ? 200 : 50
}
repeat: false
onTriggered: {
BrightnessService.setBrightnessInternal(pendingValue, BrightnessService.lastIpcDevice)
}
}
Connections {
target: BrightnessService
function onBrightnessChanged() {
root.show()
}
}
content: Item {
anchors.fill: parent
Item {
property int gap: Theme.spacingS
anchors.centerIn: parent
width: parent.width - Theme.spacingS * 2
height: 40
Rectangle {
width: Theme.iconSize
height: Theme.iconSize
radius: Theme.iconSize / 2
color: "transparent"
x: parent.gap
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: {
const deviceInfo = BrightnessService.getCurrentDeviceInfo()
if (!deviceInfo || deviceInfo.class === "backlight" || deviceInfo.class === "ddc")
return "brightness_medium"
else if (deviceInfo.name.includes("kbd"))
return "keyboard"
else
return "lightbulb"
}
size: Theme.iconSize
color: Theme.primary
}
}
DankSlider {
id: brightnessSlider
width: parent.width - Theme.iconSize - parent.gap * 3
height: 40
x: parent.gap * 2 + Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
minimum: 1
maximum: 100
enabled: BrightnessService.brightnessAvailable
showValue: true
unit: "%"
Component.onCompleted: {
if (BrightnessService.brightnessAvailable)
value = BrightnessService.brightnessLevel
}
onSliderValueChanged: function(newValue) {
if (BrightnessService.brightnessAvailable) {
root.brightnessDebounceTimer.pendingValue = newValue
root.brightnessDebounceTimer.restart()
resetHideTimer()
}
}
onContainsMouseChanged: {
setChildHovered(containsMouse)
}
onSliderDragFinished: function(finalValue) {
if (BrightnessService.brightnessAvailable) {
root.brightnessDebounceTimer.stop()
BrightnessService.setBrightnessInternal(finalValue, BrightnessService.lastIpcDevice)
}
}
Connections {
target: BrightnessService
function onBrightnessChanged() {
if (!brightnessSlider.pressed)
brightnessSlider.value = BrightnessService.brightnessLevel
}
function onDeviceSwitched() {
if (!brightnessSlider.pressed)
brightnessSlider.value = BrightnessService.brightnessLevel
}
}
}
}
}
onOsdShown: {
if (BrightnessService.brightnessAvailable && contentLoader.item) {
let slider = contentLoader.item.children[0].children[1]
if (slider)
slider.value = BrightnessService.brightnessLevel
}
}
}
-27
View File
@@ -1,27 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
DankOSD {
id: root
osdWidth: Theme.iconSize + Theme.spacingS * 2
osdHeight: Theme.iconSize + Theme.spacingS * 2
autoHideInterval: 2000
enableMouseInteraction: false
Connections {
target: SessionService
function onInhibitorChanged() {
root.show()
}
}
content: DankIcon {
anchors.centerIn: parent
name: SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
size: Theme.iconSize
color: SessionService.idleInhibited ? Theme.primary : Theme.outline
}
}
-27
View File
@@ -1,27 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
DankOSD {
id: root
osdWidth: Theme.iconSize + Theme.spacingS * 2
osdHeight: Theme.iconSize + Theme.spacingS * 2
autoHideInterval: 2000
enableMouseInteraction: false
Connections {
target: AudioService
function onMicMuteChanged() {
root.show()
}
}
content: DankIcon {
anchors.centerIn: parent
name: AudioService.source && AudioService.source.audio && AudioService.source.audio.muted ? "mic_off" : "mic"
size: Theme.iconSize
color: AudioService.source && AudioService.source.audio && AudioService.source.audio.muted ? Theme.error : Theme.primary
}
}
-117
View File
@@ -1,117 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
DankOSD {
id: root
osdWidth: Math.min(260, Screen.width - Theme.spacingM * 2)
osdHeight: 40 + Theme.spacingS * 2
autoHideInterval: 3000
enableMouseInteraction: true
Connections {
target: AudioService
function onVolumeChanged() {
root.show()
}
function onSinkChanged() {
if (root.shouldBeVisible)
root.show()
}
}
content: Item {
anchors.fill: parent
Item {
property int gap: Theme.spacingS
anchors.centerIn: parent
width: parent.width - Theme.spacingS * 2
height: 40
Rectangle {
width: Theme.iconSize
height: Theme.iconSize
radius: Theme.iconSize / 2
color: "transparent"
x: parent.gap
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.muted ? "volume_off" : "volume_up"
size: Theme.iconSize
color: muteButton.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: muteButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
AudioService.toggleMute()
resetHideTimer()
}
onContainsMouseChanged: {
setChildHovered(containsMouse || volumeSlider.containsMouse)
}
}
}
DankSlider {
id: volumeSlider
width: parent.width - Theme.iconSize - parent.gap * 3
height: 40
x: parent.gap * 2 + Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
minimum: 0
maximum: 100
enabled: AudioService.sink && AudioService.sink.audio
showValue: true
unit: "%"
Component.onCompleted: {
if (AudioService.sink && AudioService.sink.audio)
value = Math.round(AudioService.sink.audio.volume * 100)
}
onSliderValueChanged: function(newValue) {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.volume = newValue / 100
resetHideTimer()
}
}
onContainsMouseChanged: {
setChildHovered(containsMouse || muteButton.containsMouse)
}
Connections {
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
function onVolumeChanged() {
if (volumeSlider && !volumeSlider.pressed)
volumeSlider.value = Math.round(AudioService.sink.audio.volume * 100)
}
}
}
}
}
onOsdShown: {
if (AudioService.sink && AudioService.sink.audio && contentLoader.item) {
let slider = contentLoader.item.children[0].children[1]
if (slider)
slider.value = Math.round(AudioService.sink.audio.volume * 100)
}
}
}
-483
View File
@@ -1,483 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Column {
function formatNetworkSpeed(bytesPerSec) {
if (bytesPerSec < 1024)
return bytesPerSec.toFixed(0) + " B/s"
else if (bytesPerSec < 1024 * 1024)
return (bytesPerSec / 1024).toFixed(1) + " KB/s"
else if (bytesPerSec < 1024 * 1024 * 1024)
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s"
else
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s"
}
function formatDiskSpeed(bytesPerSec) {
if (bytesPerSec < 1024 * 1024)
return (bytesPerSec / 1024).toFixed(1) + " KB/s"
else if (bytesPerSec < 1024 * 1024 * 1024)
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s"
else
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s"
}
anchors.fill: parent
spacing: Theme.spacingM
Component.onCompleted: {
DgopService.addRef(["cpu", "memory", "network", "disk"])
}
Component.onDestruction: {
DgopService.removeRef(["cpu", "memory", "network", "disk"])
}
Rectangle {
width: parent.width
height: 200
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.04)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.06)
border.width: 1
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
width: parent.width
height: 32
spacing: Theme.spacingM
StyledText {
text: "CPU"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: 80
height: 24
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12)
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: DgopService.cpuUsage.toFixed(1) + "%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.primary
anchors.centerIn: parent
}
}
Item {
width: parent.width - 280
height: 1
}
StyledText {
text: DgopService.cpuCores + " cores"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
ScrollView {
width: parent.width
height: parent.height - 40
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Column {
width: parent.width
spacing: 6
Repeater {
model: DgopService.perCoreCpuUsage
Row {
width: parent.width
height: 20
spacing: Theme.spacingS
StyledText {
text: "C" + index
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 24
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: parent.width - 80
height: 6
radius: 3
color: Qt.rgba(Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.2)
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: parent.width * Math.min(
1, modelData / 100)
height: parent.height
radius: parent.radius
color: {
const usage = modelData
if (usage > 80)
return Theme.error
if (usage > 60)
return Theme.warning
return Theme.primary
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
}
}
}
}
StyledText {
text: modelData ? modelData.toFixed(
0) + "%" : "0%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
width: 32
horizontalAlignment: Text.AlignRight
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
Rectangle {
width: parent.width
height: 80
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.04)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.06)
border.width: 1
Row {
anchors.centerIn: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
StyledText {
text: "Memory"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: DgopService.formatSystemMemory(
DgopService.usedMemoryKB) + " / " + DgopService.formatSystemMemory(
DgopService.totalMemoryKB)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Item {
width: Theme.spacingL
height: 1
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
width: 200
Rectangle {
width: parent.width
height: 16
radius: 8
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
Rectangle {
width: DgopService.totalMemoryKB
> 0 ? parent.width * (DgopService.usedMemoryKB
/ DgopService.totalMemoryKB) : 0
height: parent.height
radius: parent.radius
color: {
const usage = DgopService.totalMemoryKB
> 0 ? (DgopService.usedMemoryKB
/ DgopService.totalMemoryKB) : 0
if (usage > 0.9)
return Theme.error
if (usage > 0.7)
return Theme.warning
return Theme.secondary
}
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
}
}
}
}
StyledText {
text: DgopService.totalMemoryKB
> 0 ? ((DgopService.usedMemoryKB
/ DgopService.totalMemoryKB) * 100).toFixed(
1) + "% used" : "No data"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
Item {
width: Theme.spacingL
height: 1
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
StyledText {
text: "Swap"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: DgopService.totalSwapKB
> 0 ? DgopService.formatSystemMemory(
DgopService.usedSwapKB) + " / "
+ DgopService.formatSystemMemory(
DgopService.totalSwapKB) : "No swap configured"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Item {
width: Theme.spacingL
height: 1
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
width: 200
Rectangle {
width: parent.width
height: 16
radius: 8
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
Rectangle {
width: DgopService.totalSwapKB
> 0 ? parent.width * (DgopService.usedSwapKB
/ DgopService.totalSwapKB) : 0
height: parent.height
radius: parent.radius
color: {
if (!DgopService.totalSwapKB)
return Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
const usage = DgopService.usedSwapKB / DgopService.totalSwapKB
if (usage > 0.9)
return Theme.error
if (usage > 0.7)
return Theme.warning
return Theme.info
}
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
}
}
}
}
StyledText {
text: DgopService.totalSwapKB
> 0 ? ((DgopService.usedSwapKB / DgopService.totalSwapKB) * 100).toFixed(
1) + "% used" : "Not available"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
Row {
width: parent.width
height: 80
spacing: Theme.spacingM
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.04)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.06)
border.width: 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: "Network"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: 4
StyledText {
text: "↓"
font.pixelSize: Theme.fontSizeSmall
color: Theme.info
}
StyledText {
text: DgopService.networkRxRate
> 0 ? formatNetworkSpeed(
DgopService.networkRxRate) : "0 B/s"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
Row {
spacing: 4
StyledText {
text: "↑"
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
}
StyledText {
text: DgopService.networkTxRate
> 0 ? formatNetworkSpeed(
DgopService.networkTxRate) : "0 B/s"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.04)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.06)
border.width: 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: "Disk"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: 4
StyledText {
text: "R"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
}
StyledText {
text: formatDiskSpeed(DgopService.diskReadRate)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
Row {
spacing: 4
StyledText {
text: "W"
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
}
StyledText {
text: formatDiskSpeed(DgopService.diskWriteRate)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
}
}
}
-252
View File
@@ -1,252 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
Popup {
id: processContextMenu
property var processData: null
function show(x, y) {
if (!processContextMenu.parent && typeof Overlay !== "undefined"
&& Overlay.overlay)
processContextMenu.parent = Overlay.overlay
const menuWidth = 180
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
const screenWidth = Screen.width
const screenHeight = Screen.height
let finalX = x
let finalY = y
if (x + menuWidth > screenWidth - 20)
finalX = x - menuWidth
if (y + menuHeight > screenHeight - 20)
finalY = y - menuHeight
processContextMenu.x = Math.max(20, finalX)
processContextMenu.y = Math.max(20, finalY)
open()
}
width: 180
height: menuColumn.implicitHeight + Theme.spacingS * 2
padding: 0
modal: false
closePolicy: Popup.CloseOnEscape
onClosed: {
closePolicy = Popup.CloseOnEscape
}
onOpened: {
outsideClickTimer.start()
}
Timer {
id: outsideClickTimer
interval: 100
onTriggered: {
processContextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside
}
}
background: Rectangle {
color: "transparent"
}
contentItem: Rectangle {
id: menuContent
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadius
color: copyPidArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Copy PID"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
}
MouseArea {
id: copyPidArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (processContextMenu.processData)
Quickshell.execDetached(
["wl-copy", processContextMenu.processData.pid.toString(
)])
processContextMenu.close()
}
}
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadius
color: copyNameArea.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Copy Process Name"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
}
MouseArea {
id: copyNameArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (processContextMenu.processData) {
let processName = processContextMenu.processData.displayName
|| processContextMenu.processData.command
Quickshell.execDetached(["wl-copy", processName])
}
processContextMenu.close()
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadius
color: killArea.containsMouse ? Qt.rgba(Theme.error.r,
Theme.error.g,
Theme.error.b,
0.12) : "transparent"
enabled: processContextMenu.processData
opacity: enabled ? 1 : 0.5
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Kill Process"
font.pixelSize: Theme.fontSizeSmall
color: parent.enabled ? (killArea.containsMouse ? Theme.error : Theme.surfaceText) : Qt.rgba(
Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.5)
font.weight: Font.Normal
}
MouseArea {
id: killArea
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: parent.enabled
onClicked: {
if (processContextMenu.processData)
Quickshell.execDetached(
["kill", processContextMenu.processData.pid.toString(
)])
processContextMenu.close()
}
}
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadius
color: forceKillArea.containsMouse ? Qt.rgba(
Theme.error.r,
Theme.error.g,
Theme.error.b,
0.12) : "transparent"
enabled: processContextMenu.processData
&& processContextMenu.processData.pid > 1000
opacity: enabled ? 1 : 0.5
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Force Kill Process"
font.pixelSize: Theme.fontSizeSmall
color: parent.enabled ? (forceKillArea.containsMouse ? Theme.error : Theme.surfaceText) : Qt.rgba(
Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.5)
font.weight: Font.Normal
}
MouseArea {
id: forceKillArea
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: parent.enabled
onClicked: {
if (processContextMenu.processData)
Quickshell.execDetached(
["kill", "-9", processContextMenu.processData.pid.toString(
)])
processContextMenu.close()
}
}
}
}
}
}
-232
View File
@@ -1,232 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: processItem
property var process: null
property var contextMenu: null
width: parent ? parent.width : 0
height: 40
radius: Theme.cornerRadius
color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.08) : "transparent"
border.color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
border.width: 1
MouseArea {
id: processMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
if (process && process.pid > 0 && contextMenu) {
contextMenu.processData = process
let globalPos = processMouseArea.mapToGlobal(
mouse.x, mouse.y)
let localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(
globalPos.x,
globalPos.y) : globalPos
contextMenu.show(localPos.x, localPos.y)
}
}
}
onPressAndHold: {
if (process && process.pid > 0 && contextMenu) {
contextMenu.processData = process
let globalPos = processMouseArea.mapToGlobal(
processMouseArea.width / 2, processMouseArea.height / 2)
contextMenu.show(globalPos.x, globalPos.y)
}
}
}
Item {
anchors.fill: parent
anchors.margins: 8
DankIcon {
id: processIcon
name: DgopService.getProcessIcon(process ? process.command : "")
size: Theme.iconSize - 4
color: {
if (process && process.cpu > 80)
return Theme.error
if (process && process.cpu > 50)
return Theme.warning
return Theme.surfaceText
}
opacity: 0.8
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: process ? process.displayName : ""
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
width: 250
elide: Text.ElideRight
anchors.left: processIcon.right
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: cpuBadge
width: 80
height: 20
radius: Theme.cornerRadius
color: {
if (process && process.cpu > 80)
return Qt.rgba(Theme.error.r, Theme.error.g,
Theme.error.b, 0.12)
if (process && process.cpu > 50)
return Qt.rgba(Theme.warning.r, Theme.warning.g,
Theme.warning.b, 0.12)
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.08)
}
anchors.right: parent.right
anchors.rightMargin: 194
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: DgopService.formatCpuUsage(process ? process.cpu : 0)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: {
if (process && process.cpu > 80)
return Theme.error
if (process && process.cpu > 50)
return Theme.warning
return Theme.surfaceText
}
anchors.centerIn: parent
}
}
Rectangle {
id: memoryBadge
width: 80
height: 20
radius: Theme.cornerRadius
color: {
if (process && process.memoryKB > 1024 * 1024)
return Qt.rgba(Theme.error.r, Theme.error.g,
Theme.error.b, 0.12)
if (process && process.memoryKB > 512 * 1024)
return Qt.rgba(Theme.warning.r, Theme.warning.g,
Theme.warning.b, 0.12)
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.08)
}
anchors.right: parent.right
anchors.rightMargin: 102
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: DgopService.formatMemoryUsage(
process ? process.memoryKB : 0)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: {
if (process && process.memoryKB > 1024 * 1024)
return Theme.error
if (process && process.memoryKB > 512 * 1024)
return Theme.warning
return Theme.surfaceText
}
anchors.centerIn: parent
}
}
StyledText {
text: process ? process.pid.toString() : ""
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
width: 50
horizontalAlignment: Text.AlignRight
anchors.right: parent.right
anchors.rightMargin: 40
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: menuButton
width: 28
height: 28
radius: Theme.cornerRadius
color: menuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.08) : "transparent"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "more_vert"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.6
anchors.centerIn: parent
}
MouseArea {
id: menuButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (process && process.pid > 0 && contextMenu) {
contextMenu.processData = process
let globalPos = menuButtonArea.mapToGlobal(
menuButtonArea.width / 2, menuButtonArea.height)
let localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(
globalPos.x,
globalPos.y) : globalPos
contextMenu.show(localPos.x, localPos.y)
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
-138
View File
@@ -1,138 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules.ProcessList
import qs.Services
import qs.Widgets
DankPopout {
id: processListPopout
property var parentWidget: null
property string triggerSection: "right"
property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen) {
triggerX = x
triggerY = y
triggerWidth = width
triggerSection = section
triggerScreen = screen
}
function hide() {
close()
if (processContextMenu.visible)
processContextMenu.close()
}
function show() {
open()
}
popupWidth: 600
popupHeight: 600
triggerX: Screen.width - 600 - Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
triggerWidth: 55
positioning: "center"
WlrLayershell.namespace: "quickshell-processlist"
screen: triggerScreen
visible: shouldBeVisible
shouldBeVisible: false
Ref {
service: DgopService
}
content: Component {
Rectangle {
id: processListContent
radius: Theme.cornerRadius
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
clip: true
antialiasing: true
smooth: true
focus: true
Component.onCompleted: {
if (processListPopout.shouldBeVisible)
forceActiveFocus()
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
processListPopout.close()
event.accepted = true
}
}
Connections {
function onShouldBeVisibleChanged() {
if (processListPopout.shouldBeVisible)
Qt.callLater(function () {
processListContent.forceActiveFocus()
})
}
target: processListPopout
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Rectangle {
Layout.fillWidth: true
height: systemOverview.height + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.2)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
SystemOverview {
id: systemOverview
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.1)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.05)
border.width: 1
ProcessListView {
anchors.fill: parent
anchors.margins: Theme.spacingS
contextMenu: processContextMenu
}
}
}
}
}
ProcessContextMenu {
id: processContextMenu
}
}
-270
View File
@@ -1,270 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Column {
id: root
property var contextMenu: null
Component.onCompleted: {
DgopService.addRef(["processes"])
}
Component.onDestruction: {
DgopService.removeRef(["processes"])
}
Item {
id: columnHeaders
width: parent.width
anchors.leftMargin: 8
height: 24
Rectangle {
width: 60
height: 20
color: {
if (DgopService.currentSort === "name") {
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12)
}
return processHeaderArea.containsMouse ? Qt.rgba(
Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.08) : "transparent"
}
radius: Theme.cornerRadius
anchors.left: parent.left
anchors.leftMargin: 0
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Process"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: DgopService.currentSort === "name" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: DgopService.currentSort === "name" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: processHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
DgopService.setSortBy("name")
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: 80
height: 20
color: {
if (DgopService.currentSort === "cpu") {
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12)
}
return cpuHeaderArea.containsMouse ? Qt.rgba(
Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.08) : "transparent"
}
radius: Theme.cornerRadius
anchors.right: parent.right
anchors.rightMargin: 200
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "CPU"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: DgopService.currentSort === "cpu" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: DgopService.currentSort === "cpu" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: cpuHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
DgopService.setSortBy("cpu")
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: 80
height: 20
color: {
if (DgopService.currentSort === "memory") {
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12)
}
return memoryHeaderArea.containsMouse ? Qt.rgba(
Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.08) : "transparent"
}
radius: Theme.cornerRadius
anchors.right: parent.right
anchors.rightMargin: 112
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "RAM"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: DgopService.currentSort === "memory" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: DgopService.currentSort === "memory" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: memoryHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
DgopService.setSortBy("memory")
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: 50
height: 20
color: {
if (DgopService.currentSort === "pid") {
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12)
}
return pidHeaderArea.containsMouse ? Qt.rgba(
Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.08) : "transparent"
}
radius: Theme.cornerRadius
anchors.right: parent.right
anchors.rightMargin: 53
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "PID"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: DgopService.currentSort === "pid" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: DgopService.currentSort === "pid" ? 1 : 0.7
horizontalAlignment: Text.AlignHCenter
anchors.centerIn: parent
}
MouseArea {
id: pidHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
DgopService.setSortBy("pid")
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: 28
height: 28
radius: Theme.cornerRadius
color: sortOrderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.08) : "transparent"
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: DgopService.sortDescending ? "↓" : "↑"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
id: sortOrderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// ! TODO - we lost this with dgop
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
DankListView {
id: processListView
property string keyRoleName: "pid"
width: parent.width
height: parent.height - columnHeaders.height
clip: true
spacing: 4
model: DgopService.processes
delegate: ProcessListItem {
process: modelData
contextMenu: root.contextMenu
}
}
}
-28
View File
@@ -1,28 +0,0 @@
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Modules.ProcessList
import qs.Services
ColumnLayout {
id: processesTab
property var contextMenu: null
anchors.fill: parent
spacing: Theme.spacingM
SystemOverview {
Layout.fillWidth: true
}
ProcessListView {
Layout.fillWidth: true
Layout.fillHeight: true
contextMenu: processesTab.contextMenu || localContextMenu
}
ProcessContextMenu {
id: localContextMenu
}
}
-491
View File
@@ -1,491 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Row {
width: parent.width
spacing: Theme.spacingM
Component.onCompleted: {
DgopService.addRef(["cpu", "memory", "system"])
}
Component.onDestruction: {
DgopService.removeRef(["cpu", "memory", "system"])
}
Rectangle {
width: (parent.width - Theme.spacingM * 2) / 3
height: 80
radius: Theme.cornerRadius
color: {
if (DgopService.sortBy === "cpu")
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.16)
else if (cpuCardMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12)
else
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.08)
}
border.color: DgopService.sortBy === "cpu" ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.4) : Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b, 0.2)
border.width: DgopService.sortBy === "cpu" ? 2 : 1
MouseArea {
id: cpuCardMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: DgopService.setSortBy("cpu")
}
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: "CPU"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: DgopService.sortBy === "cpu" ? Theme.primary : Theme.secondary
opacity: DgopService.sortBy === "cpu" ? 1 : 0.8
}
Row {
spacing: Theme.spacingS
StyledText {
text: {
if (DgopService.cpuUsage === undefined
|| DgopService.cpuUsage === null)
return "--%"
return DgopService.cpuUsage.toFixed(1) + "%"
}
font.pixelSize: Theme.fontSizeLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: 1
height: 20
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (DgopService.cpuTemperature === undefined
|| DgopService.cpuTemperature === null
|| DgopService.cpuTemperature <= 0)
return "--°"
return Math.round(DgopService.cpuTemperature) + "°"
}
font.pixelSize: Theme.fontSizeMedium
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: {
if (DgopService.cpuTemperature > 80)
return Theme.error
if (DgopService.cpuTemperature > 60)
return Theme.warning
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: DgopService.cpuCores + " cores"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM * 2) / 3
height: 80
radius: Theme.cornerRadius
color: {
if (DgopService.sortBy === "memory")
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.16)
else if (memoryCardMouseArea.containsMouse)
return Qt.rgba(Theme.secondary.r, Theme.secondary.g,
Theme.secondary.b, 0.12)
else
return Qt.rgba(Theme.secondary.r, Theme.secondary.g,
Theme.secondary.b, 0.08)
}
border.color: DgopService.sortBy === "memory" ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.4) : Qt.rgba(
Theme.secondary.r,
Theme.secondary.g,
Theme.secondary.b,
0.2)
border.width: DgopService.sortBy === "memory" ? 2 : 1
MouseArea {
id: memoryCardMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: DgopService.setSortBy("memory")
}
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: "Memory"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: DgopService.sortBy === "memory" ? Theme.primary : Theme.secondary
opacity: DgopService.sortBy === "memory" ? 1 : 0.8
}
Row {
spacing: Theme.spacingS
StyledText {
text: DgopService.formatSystemMemory(
DgopService.usedMemoryKB)
font.pixelSize: Theme.fontSizeLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: 1
height: 20
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
anchors.verticalCenter: parent.verticalCenter
visible: DgopService.totalSwapKB > 0
}
StyledText {
text: DgopService.totalSwapKB > 0 ? DgopService.formatSystemMemory(
DgopService.usedSwapKB) : ""
font.pixelSize: Theme.fontSizeMedium
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: DgopService.usedSwapKB > 0 ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: DgopService.totalSwapKB > 0
}
}
StyledText {
text: {
if (DgopService.totalSwapKB > 0)
return "of " + DgopService.formatSystemMemory(
DgopService.totalMemoryKB) + " + swap"
return "of " + DgopService.formatSystemMemory(
DgopService.totalMemoryKB)
}
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM * 2) / 3
height: 80
radius: Theme.cornerRadius
color: {
if (!DgopService.availableGpus
|| DgopService.availableGpus.length === 0) {
if (gpuCardMouseArea.containsMouse
&& DgopService.availableGpus.length > 1)
return Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.16)
else
return Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.08)
}
var gpu = DgopService.availableGpus[Math.min(
SessionData.selectedGpuIndex,
DgopService.availableGpus.length - 1)]
var vendor = gpu.vendor.toLowerCase()
if (vendor.includes("nvidia")) {
if (gpuCardMouseArea.containsMouse
&& DgopService.availableGpus.length > 1)
return Qt.rgba(Theme.success.r, Theme.success.g,
Theme.success.b, 0.2)
else
return Qt.rgba(Theme.success.r, Theme.success.g,
Theme.success.b, 0.12)
} else if (vendor.includes("amd")) {
if (gpuCardMouseArea.containsMouse
&& DgopService.availableGpus.length > 1)
return Qt.rgba(Theme.error.r, Theme.error.g,
Theme.error.b, 0.2)
else
return Qt.rgba(Theme.error.r, Theme.error.g,
Theme.error.b, 0.12)
} else if (vendor.includes("intel")) {
if (gpuCardMouseArea.containsMouse
&& DgopService.availableGpus.length > 1)
return Qt.rgba(Theme.info.r, Theme.info.g,
Theme.info.b, 0.2)
else
return Qt.rgba(Theme.info.r, Theme.info.g,
Theme.info.b, 0.12)
}
if (gpuCardMouseArea.containsMouse
&& DgopService.availableGpus.length > 1)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.16)
else
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.08)
}
border.color: {
if (!DgopService.availableGpus
|| DgopService.availableGpus.length === 0)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.2)
var gpu = DgopService.availableGpus[Math.min(
SessionData.selectedGpuIndex,
DgopService.availableGpus.length - 1)]
var vendor = gpu.vendor.toLowerCase()
if (vendor.includes("nvidia"))
return Qt.rgba(Theme.success.r, Theme.success.g,
Theme.success.b, 0.3)
else if (vendor.includes("amd"))
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3)
else if (vendor.includes("intel"))
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.3)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.2)
}
border.width: 1
MouseArea {
id: gpuCardMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: DgopService.availableGpus.length
> 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (DgopService.availableGpus.length > 1) {
var nextIndex = (SessionData.selectedGpuIndex + 1)
% DgopService.availableGpus.length
SessionData.setSelectedGpuIndex(nextIndex)
}
} else if (mouse.button === Qt.RightButton) {
gpuContextMenu.popup()
}
}
}
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: "GPU"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.secondary
opacity: 0.8
}
StyledText {
text: {
if (!DgopService.availableGpus
|| DgopService.availableGpus.length === 0)
return "No GPU"
var gpu = DgopService.availableGpus[Math.min(
SessionData.selectedGpuIndex,
DgopService.availableGpus.length - 1)]
// Check if temperature monitoring is enabled for this GPU
var tempEnabled = SessionData.enabledGpuPciIds
&& SessionData.enabledGpuPciIds.indexOf(
gpu.pciId) !== -1
var temp = gpu.temperature
var hasTemp = tempEnabled && temp !== undefined
&& temp !== null && temp !== 0
if (hasTemp)
return Math.round(temp) + "°"
else
return gpu.vendor
}
font.pixelSize: Theme.fontSizeLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: {
if (!DgopService.availableGpus
|| DgopService.availableGpus.length === 0)
return Theme.surfaceText
var gpu = DgopService.availableGpus[Math.min(
SessionData.selectedGpuIndex,
DgopService.availableGpus.length - 1)]
var tempEnabled = SessionData.enabledGpuPciIds
&& SessionData.enabledGpuPciIds.indexOf(
gpu.pciId) !== -1
var temp = gpu.temperature || 0
if (tempEnabled && temp > 80)
return Theme.error
if (tempEnabled && temp > 60)
return Theme.warning
return Theme.surfaceText
}
}
StyledText {
text: {
if (!DgopService.availableGpus
|| DgopService.availableGpus.length === 0)
return "No GPUs detected"
var gpu = DgopService.availableGpus[Math.min(
SessionData.selectedGpuIndex,
DgopService.availableGpus.length - 1)]
var tempEnabled = SessionData.enabledGpuPciIds
&& SessionData.enabledGpuPciIds.indexOf(
gpu.pciId) !== -1
var temp = gpu.temperature
var hasTemp = tempEnabled && temp !== undefined
&& temp !== null && temp !== 0
if (hasTemp)
return gpu.vendor + " " + gpu.displayName
else
return gpu.displayName
}
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
width: parent.parent.width - Theme.spacingM * 2
elide: Text.ElideRight
maximumLineCount: 1
}
}
Menu {
id: gpuContextMenu
MenuItem {
text: "Enable GPU Temperature"
checkable: true
checked: {
if (!DgopService.availableGpus
|| DgopService.availableGpus.length === 0) {
return false
}
var gpu = DgopService.availableGpus[Math.min(
SessionData.selectedGpuIndex,
DgopService.availableGpus.length - 1)]
if (!gpu.pciId)
return false
return SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.indexOf(
gpu.pciId) !== -1 : false
}
onTriggered: {
if (!DgopService.availableGpus
|| DgopService.availableGpus.length === 0) {
return
}
var gpu = DgopService.availableGpus[Math.min(
SessionData.selectedGpuIndex,
DgopService.availableGpus.length - 1)]
if (!gpu.pciId)
return
var enabledIds = SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.slice(
) : []
var index = enabledIds.indexOf(gpu.pciId)
if (checked && index === -1) {
enabledIds.push(gpu.pciId)
DgopService.addGpuPciId(gpu.pciId)
} else if (!checked && index !== -1) {
enabledIds.splice(index, 1)
DgopService.removeGpuPciId(gpu.pciId)
}
SessionData.setEnabledGpuPciIds(enabledIds)
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
-671
View File
@@ -1,671 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
ScrollView {
anchors.fill: parent
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Component.onCompleted: {
DgopService.addRef(["system", "hardware", "diskmounts"])
}
Component.onDestruction: {
DgopService.removeRef(["system", "hardware", "diskmounts"])
}
Column {
width: parent.width
spacing: Theme.spacingM
Rectangle {
width: parent.width
height: systemInfoColumn.implicitHeight + 2 * Theme.spacingL
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.6)
border.width: 0
Column {
id: systemInfoColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Row {
width: parent.width
spacing: Theme.spacingL
SystemLogo {
width: 80
height: 80
}
Column {
width: parent.width - 80 - Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
StyledText {
text: DgopService.hostname
font.pixelSize: Theme.fontSizeXLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Light
color: Theme.surfaceText
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: DgopService.distribution + " • " + DgopService.architecture
+ " • " + DgopService.kernelVersion
font.pixelSize: Theme.fontSizeMedium
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Up " + UserInfoService.uptime + " • Boot: "
+ DgopService.bootTime
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.6)
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Load: " + DgopService.loadAverage + " • "
+ DgopService.processCount + " processes, "
+ DgopService.threadCount + " threads"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.6)
verticalAlignment: Text.AlignVCenter
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
}
Row {
width: parent.width
spacing: Theme.spacingXL
Rectangle {
width: (parent.width - Theme.spacingXL) / 2
height: hardwareColumn.implicitHeight + Theme.spacingL
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainerHigh.r,
Theme.surfaceContainerHigh.g,
Theme.surfaceContainerHigh.b, 0.4)
border.width: 1
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.1)
Column {
id: hardwareColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "memory"
size: Theme.iconSizeSmall
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "System"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: DgopService.cpuModel
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: DgopService.motherboard
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "BIOS " + DgopService.biosVersion
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: DgopService.formatSystemMemory(
DgopService.totalMemoryKB) + " RAM"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
}
}
Rectangle {
width: (parent.width - Theme.spacingXL) / 2
height: gpuColumn.implicitHeight + Theme.spacingL
radius: Theme.cornerRadius
color: {
var baseColor = Qt.rgba(
Theme.surfaceContainerHigh.r,
Theme.surfaceContainerHigh.g,
Theme.surfaceContainerHigh.b, 0.4)
var hoverColor = Qt.rgba(
Theme.surfaceContainerHigh.r,
Theme.surfaceContainerHigh.g,
Theme.surfaceContainerHigh.b, 0.6)
if (!DgopService.availableGpus
|| DgopService.availableGpus.length === 0) {
return gpuCardMouseArea.containsMouse
&& DgopService.availableGpus.length
> 1 ? hoverColor : baseColor
}
var gpu = DgopService.availableGpus[Math.min(
SessionData.selectedGpuIndex,
DgopService.availableGpus.length
- 1)]
var vendor = gpu.fullName.split(
' ')[0].toLowerCase()
var tintColor
if (vendor.includes("nvidia")) {
tintColor = Theme.success
} else if (vendor.includes("amd")) {
tintColor = Theme.error
} else if (vendor.includes("intel")) {
tintColor = Theme.info
} else {
return gpuCardMouseArea.containsMouse
&& DgopService.availableGpus.length
> 1 ? hoverColor : baseColor
}
if (gpuCardMouseArea.containsMouse
&& DgopService.availableGpus.length > 1) {
return Qt.rgba(
(hoverColor.r + tintColor.r * 0.1) / 1.1,
(hoverColor.g + tintColor.g * 0.1) / 1.1,
(hoverColor.b + tintColor.b * 0.1) / 1.1,
0.6)
} else {
return Qt.rgba(
(baseColor.r + tintColor.r * 0.08) / 1.08,
(baseColor.g + tintColor.g * 0.08) / 1.08,
(baseColor.b + tintColor.b * 0.08) / 1.08,
0.4)
}
}
border.width: 1
border.color: {
if (!DgopService.availableGpus
|| DgopService.availableGpus.length === 0) {
return Qt.rgba(Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.1)
}
var gpu = DgopService.availableGpus[Math.min(
SessionData.selectedGpuIndex,
DgopService.availableGpus.length
- 1)]
var vendor = gpu.fullName.split(
' ')[0].toLowerCase()
if (vendor.includes("nvidia")) {
return Qt.rgba(Theme.success.r,
Theme.success.g,
Theme.success.b, 0.3)
} else if (vendor.includes("amd")) {
return Qt.rgba(Theme.error.r, Theme.error.g,
Theme.error.b, 0.3)
} else if (vendor.includes("intel")) {
return Qt.rgba(Theme.info.r, Theme.info.g,
Theme.info.b, 0.3)
}
return Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.1)
}
MouseArea {
id: gpuCardMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DgopService.availableGpus.length
> 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (DgopService.availableGpus.length > 1) {
var nextIndex = (SessionData.selectedGpuIndex + 1)
% DgopService.availableGpus.length
SessionData.setSelectedGpuIndex(nextIndex)
}
}
}
Column {
id: gpuColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "auto_awesome_mosaic"
size: Theme.iconSizeSmall
color: Theme.secondary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "GPU"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.secondary
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: {
if (!DgopService.availableGpus
|| DgopService.availableGpus.length === 0) {
return "No GPUs detected"
}
var gpu = DgopService.availableGpus[Math.min(
SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]
return gpu.fullName
}
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
maximumLineCount: 1
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: {
if (!DgopService.availableGpus
|| DgopService.availableGpus.length === 0) {
return "Device: N/A"
}
var gpu = DgopService.availableGpus[Math.min(
SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]
return "Device: " + gpu.pciId
}
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
textFormat: Text.RichText
}
StyledText {
text: {
if (!DgopService.availableGpus
|| DgopService.availableGpus.length === 0) {
return "Driver: N/A"
}
var gpu = DgopService.availableGpus[Math.min(
SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]
return "Driver: " + gpu.driver
}
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: {
if (!DgopService.availableGpus
|| DgopService.availableGpus.length === 0) {
return "Temp: --°"
}
var gpu = DgopService.availableGpus[Math.min(
SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]
var temp = gpu.temperature
return "Temp: " + ((temp === undefined
|| temp === null
|| temp === 0) ? "--°" : Math.round(
temp) + "°C")
}
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: {
if (!DgopService.availableGpus
|| DgopService.availableGpus.length === 0) {
return Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
}
var gpu = DgopService.availableGpus[Math.min(
SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]
var temp = gpu.temperature || 0
if (temp > 80)
return Theme.error
if (temp > 60)
return Theme.warning
return Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
}
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
}
Rectangle {
width: parent.width
height: storageColumn.implicitHeight + 2 * Theme.spacingL
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.6)
border.width: 0
Column {
id: storageColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingL
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "storage"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Storage & Disks"
font.pixelSize: Theme.fontSizeLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: 2
Row {
width: parent.width
height: 24
spacing: Theme.spacingS
StyledText {
text: "Device"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.25
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Mount"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.2
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Size"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Used"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Available"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Use%"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.1
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
}
Repeater {
id: diskMountRepeater
model: DgopService.diskMounts
Rectangle {
width: parent.width
height: 24
radius: Theme.cornerRadius
color: diskMouseArea.containsMouse ? Qt.rgba(
Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.04) : "transparent"
MouseArea {
id: diskMouseArea
anchors.fill: parent
hoverEnabled: true
}
Row {
anchors.fill: parent
spacing: Theme.spacingS
StyledText {
text: modelData.device
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.25
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.mount
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.2
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.size
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.used
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.avail
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.percent
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: {
const percent = parseInt(
modelData.percent)
if (percent > 90)
return Theme.error
if (percent > 75)
return Theme.warning
return Theme.surfaceText
}
width: parent.width * 0.1
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
}
}
}
}
}
-535
View File
@@ -1,535 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: aboutTab
property bool isHyprland: CompositorService.isHyprland
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
// ASCII Art Header
StyledRect {
width: parent.width
height: asciiSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: asciiSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Item {
width: parent.width
height: asciiText.implicitHeight
StyledText {
id: asciiText
text: "██████╗ █████╗ ███╗ ██╗██╗ ██╗\n██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝\n██║ ██║███████║██╔██╗ ██║█████╔╝ \n██║ ██║██╔══██║██║╚██╗██║██╔═██╗ \n██████╔╝██║ ██║██║ ╚████║██║ ██╗\n╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝"
isMonospace: true
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
anchors.centerIn: parent
}
}
StyledText {
text: "DankMaterialShell"
font.pixelSize: Theme.fontSizeXLarge
font.weight: Font.Bold
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
width: parent.width
}
Item {
id: communityIcons
anchors.horizontalCenter: parent.horizontalCenter
height: 24
width: {
if (isHyprland) {
return compositorButton.width + discordButton.width + Theme.spacingM + redditButton.width + Theme.spacingM
} else {
return compositorButton.width + matrixButton.width + 4 + discordButton.width + Theme.spacingM + redditButton.width + Theme.spacingM
}
}
// Compositor logo (Niri or Hyprland)
Item {
id: compositorButton
width: 24
height: 24
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -2
x: 0
property bool hovered: false
property string tooltipText: isHyprland ? "Hyprland Website" : "niri GitHub"
Image {
anchors.fill: parent
source: Qt.resolvedUrl(".").toString().replace(
"file://", "").replace(
"/Modules/Settings/",
"") + (isHyprland ? "/assets/hyprland.svg" : "/assets/niri.svg")
sourceSize: Qt.size(24, 24)
smooth: true
fillMode: Image.PreserveAspectFit
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: parent.hovered = true
onExited: parent.hovered = false
onClicked: Qt.openUrlExternally(
isHyprland ? "https://hypr.land" : "https://github.com/YaLTeR/niri")
}
}
// Matrix button (only for Niri)
Item {
id: matrixButton
width: 30
height: 24
x: compositorButton.x + compositorButton.width + 4
visible: !isHyprland
property bool hovered: false
property string tooltipText: "niri Matrix Chat"
Image {
anchors.fill: parent
source: Qt.resolvedUrl(".").toString().replace(
"file://", "").replace(
"/Modules/Settings/",
"") + "/assets/matrix-logo-white.svg"
sourceSize: Qt.size(28, 18)
smooth: true
fillMode: Image.PreserveAspectFit
layer.enabled: true
layer.effect: MultiEffect {
colorization: 1
colorizationColor: Theme.surfaceText
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: parent.hovered = true
onExited: parent.hovered = false
onClicked: Qt.openUrlExternally(
"https://matrix.to/#/#niri:matrix.org")
}
}
// Discord button
Item {
id: discordButton
width: 20
height: 20
x: isHyprland ? compositorButton.x + compositorButton.width + Theme.spacingM : matrixButton.x + matrixButton.width + Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
property bool hovered: false
property string tooltipText: isHyprland ? "Hyprland Discord Server" : "niri Discord Server"
Image {
anchors.fill: parent
source: Qt.resolvedUrl(".").toString().replace(
"file://", "").replace(
"/Modules/Settings/",
"") + "/assets/discord.svg"
sourceSize: Qt.size(20, 20)
smooth: true
fillMode: Image.PreserveAspectFit
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: parent.hovered = true
onExited: parent.hovered = false
onClicked: Qt.openUrlExternally(
isHyprland ? "https://discord.com/invite/hQ9XvMUjjr" : "https://discord.gg/vT8Sfjy7sx")
}
}
// Reddit button
Item {
id: redditButton
width: 20
height: 20
x: discordButton.x + discordButton.width + Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
property bool hovered: false
property string tooltipText: isHyprland ? "r/hyprland Subreddit" : "r/niri Subreddit"
Image {
anchors.fill: parent
source: Qt.resolvedUrl(".").toString().replace(
"file://", "").replace(
"/Modules/Settings/",
"") + "/assets/reddit.svg"
sourceSize: Qt.size(20, 20)
smooth: true
fillMode: Image.PreserveAspectFit
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: parent.hovered = true
onExited: parent.hovered = false
onClicked: Qt.openUrlExternally(
isHyprland ? "https://reddit.com/r/hyprland" : "https://reddit.com/r/niri")
}
}
}
}
}
// Project Information
StyledRect {
width: parent.width
height: projectSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: projectSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "info"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "About"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: `DankMaterialShell is a modern desktop inspired by <a href="https://m3.material.io/" style="text-decoration:none; color:${Theme.primary};">MUI 3</a>.
<br /><br/>The goal is to provide a high level of functionality and customization so that it can be a suitable replacement for complete desktop environments like Gnome, KDE, or Cosmic.
`
textFormat: Text.RichText
font.pixelSize: Theme.fontSizeMedium
linkColor: Theme.primary
onLinkActivated: url => Qt.openUrlExternally(url)
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
}
}
}
}
// Technical Details
StyledRect {
width: parent.width
height: techSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: techSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "code"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Technical Details"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Grid {
width: parent.width
columns: 2
columnSpacing: Theme.spacingL
rowSpacing: Theme.spacingS
StyledText {
text: "Framework:"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: `<a href="https://quickshell.org" style="text-decoration:none; color:${Theme.primary};">Quickshell</a>`
linkColor: Theme.primary
textFormat: Text.RichText
onLinkActivated: url => Qt.openUrlExternally(url)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
}
}
StyledText {
text: "Language:"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "QML (Qt Modeling Language)"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
StyledText {
text: "Compositor:"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Row {
spacing: 4
StyledText {
text: `<a href="https://github.com/YaLTeR/niri" style="text-decoration:none; color:${Theme.primary};">niri</a>`
font.pixelSize: Theme.fontSizeMedium
linkColor: Theme.primary
textFormat: Text.RichText
color: Theme.surfaceVariantText
onLinkActivated: url => Qt.openUrlExternally(url)
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
}
}
StyledText {
text: "&"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: `<a href="https://github.com/hyprwm/Hyprland" style="text-decoration:none; color:${Theme.primary};">hyprland</a>`
font.pixelSize: Theme.fontSizeMedium
linkColor: Theme.primary
textFormat: Text.RichText
color: Theme.surfaceVariantText
onLinkActivated: url => Qt.openUrlExternally(url)
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
}
}
}
StyledText {
text: "Github:"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Row {
spacing: 4
StyledText {
text: `<a href="https://github.com/AvengeMedia/DankMaterialShell" style="text-decoration:none; color:${Theme.primary};">DankMaterialShell</a>`
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
linkColor: Theme.primary
textFormat: Text.RichText
onLinkActivated: url => Qt.openUrlExternally(url)
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
}
}
StyledText {
text: "- Support Us With a Star ⭐"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: "System Monitoring:"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Row {
spacing: 4
StyledText {
text: `<a href="https://github.com/AvengeMedia/dgop" style="text-decoration:none; color:${Theme.primary};">dgop</a>`
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
linkColor: Theme.primary
textFormat: Text.RichText
onLinkActivated: url => Qt.openUrlExternally(url)
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
}
}
StyledText {
text: "- Stateless System Monitoring"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
// Community tooltip - positioned absolutely above everything
Rectangle {
id: communityTooltip
parent: aboutTab
z: 1000
property var hoveredButton: {
if (compositorButton.hovered) return compositorButton
if (matrixButton.visible && matrixButton.hovered) return matrixButton
if (discordButton.hovered) return discordButton
if (redditButton.hovered) return redditButton
return null
}
property string tooltipText: hoveredButton ? hoveredButton.tooltipText : ""
visible: hoveredButton !== null && tooltipText !== ""
width: tooltipLabel.implicitWidth + 24
height: tooltipLabel.implicitHeight + 12
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.outlineMedium
x: hoveredButton ? hoveredButton.mapToItem(aboutTab, hoveredButton.width / 2, 0).x - width / 2 : 0
y: hoveredButton ? communityIcons.mapToItem(aboutTab, 0, 0).y - height - 8 : 0
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowOpacity: 0.15
shadowVerticalOffset: 2
shadowBlur: 0.5
}
StyledText {
id: tooltipLabel
anchors.centerIn: parent
text: communityTooltip.tooltipText
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
}
}
-223
View File
@@ -1,223 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell.Widgets
import qs.Common
import qs.Widgets
Item {
id: dockTab
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
// Enable Dock
StyledRect {
width: parent.width
height: enableDockSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: enableDockSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "dock_to_bottom"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- enableToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Show Dock"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Display a dock at the bottom of the screen with pinned and running applications"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: enableToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.showDock
onToggled: checked => {
SettingsData.setShowDock(checked)
}
}
}
}
}
// Auto-hide Dock
StyledRect {
width: parent.width
height: autoHideSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
visible: SettingsData.showDock
opacity: visible ? 1 : 0
Column {
id: autoHideSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "visibility_off"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- autoHideToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Auto-hide Dock"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Hide the dock when not in use and reveal it when hovering near the bottom of the screen"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: autoHideToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.dockAutoHide
onToggled: checked => {
SettingsData.setDockAutoHide(checked)
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
// Dock Transparency Section
StyledRect {
width: parent.width
height: transparencySection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
visible: SettingsData.showDock
opacity: visible ? 1 : 0
Column {
id: transparencySection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "opacity"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Dock Transparency"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankSlider {
width: parent.width
height: 32
value: Math.round(SettingsData.dockTransparency * 100)
minimum: 0
maximum: 100
unit: "%"
showValue: true
onSliderValueChanged: newValue => {
SettingsData.setDockTransparency(
newValue / 100)
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}
-987
View File
@@ -1,987 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import qs.Common
import qs.Modals
import qs.Services
import qs.Widgets
Item {
id: personalizationTab
property alias wallpaperBrowser: wallpaperBrowser
property var parentModal: null
property var cachedFontFamilies: []
property var cachedMonoFamilies: []
property bool fontsEnumerated: false
function enumerateFonts() {
var fonts = ["Default"]
var availableFonts = Qt.fontFamilies()
var rootFamilies = []
var seenFamilies = new Set()
for (var i = 0; i < availableFonts.length; i++) {
var fontName = availableFonts[i]
if (fontName.startsWith("."))
continue
if (fontName === SettingsData.defaultFontFamily)
continue
var rootName = fontName.replace(
/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i,
"").replace(
/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
"").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i,
function (match, suffix) {
return match
}).trim()
if (!seenFamilies.has(rootName) && rootName !== "") {
seenFamilies.add(rootName)
rootFamilies.push(rootName)
}
}
cachedFontFamilies = fonts.concat(rootFamilies.sort())
var monoFonts = ["Default"]
var monoFamilies = []
var seenMonoFamilies = new Set()
for (var j = 0; j < availableFonts.length; j++) {
var fontName2 = availableFonts[j]
if (fontName2.startsWith("."))
continue
if (fontName2 === SettingsData.defaultMonoFontFamily)
continue
var lowerName = fontName2.toLowerCase()
if (lowerName.includes("mono") || lowerName.includes(
"code") || lowerName.includes(
"console") || lowerName.includes(
"terminal") || lowerName.includes(
"courier") || lowerName.includes(
"dejavu sans mono") || lowerName.includes(
"jetbrains") || lowerName.includes(
"fira") || lowerName.includes(
"hack") || lowerName.includes(
"source code") || lowerName.includes(
"ubuntu mono") || lowerName.includes("cascadia")) {
var rootName2 = fontName2.replace(
/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i,
"").replace(
/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
"").trim()
if (!seenMonoFamilies.has(rootName2) && rootName2 !== "") {
seenMonoFamilies.add(rootName2)
monoFamilies.push(rootName2)
}
}
}
cachedMonoFamilies = monoFonts.concat(monoFamilies.sort())
}
Component.onCompleted: {
// Access WallpaperCyclingService to ensure it's initialized
WallpaperCyclingService.cyclingActive
if (!fontsEnumerated) {
enumerateFonts()
fontsEnumerated = true
}
}
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
// Wallpaper Section
StyledRect {
width: parent.width
height: wallpaperSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: wallpaperSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "wallpaper"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Wallpaper"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
width: parent.width
spacing: Theme.spacingL
StyledRect {
width: 160
height: 90
radius: Theme.cornerRadius
color: Theme.surfaceVariant
border.color: Theme.outline
border.width: 1
CachingImage {
anchors.fill: parent
anchors.margins: 1
source: SessionData.wallpaperPath !== "" ? "file://" + SessionData.wallpaperPath : ""
fillMode: Image.PreserveAspectCrop
visible: SessionData.wallpaperPath !== ""
maxCacheSize: 160
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: wallpaperMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
}
Rectangle {
id: wallpaperMask
anchors.fill: parent
anchors.margins: 1
radius: Theme.cornerRadius - 1
color: "black"
visible: false
layer.enabled: true
}
DankIcon {
anchors.centerIn: parent
name: "image"
size: Theme.iconSizeLarge + 8
color: Theme.surfaceVariantText
visible: SessionData.wallpaperPath === ""
}
}
Column {
width: parent.width - 160 - Theme.spacingL
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: SessionData.wallpaperPath ? SessionData.wallpaperPath.split(
'/').pop(
) : "No wallpaper selected"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
elide: Text.ElideMiddle
maximumLineCount: 1
width: parent.width
}
StyledText {
text: SessionData.wallpaperPath ? SessionData.wallpaperPath : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideMiddle
maximumLineCount: 1
width: parent.width
visible: SessionData.wallpaperPath !== ""
}
Row {
spacing: Theme.spacingS
StyledRect {
width: 100
height: 32
radius: Theme.cornerRadius
color: Theme.primary
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "folder_open"
size: Theme.iconSizeSmall
color: Theme.primaryText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Browse"
color: Theme.primaryText
font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (parentModal) {
parentModal.allowFocusOverride = true
parentModal.shouldHaveFocus = false
}
wallpaperBrowser.open()
}
}
}
StyledRect {
width: 80
height: 32
radius: Theme.cornerRadius
color: Theme.surfaceVariant
opacity: SessionData.wallpaperPath !== "" ? 1 : 0.5
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "clear"
size: Theme.iconSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Clear"
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
enabled: SessionData.wallpaperPath !== ""
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
SessionData.setWallpaper("")
}
}
}
}
}
}
// Wallpaper Cycling Section - Full Width
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.2
visible: SessionData.wallpaperPath !== ""
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: SessionData.wallpaperPath !== ""
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "schedule"
size: Theme.iconSize
color: SessionData.wallpaperCyclingEnabled ? Theme.primary : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- controlsRow.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Wallpaper Cycling"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Automatically cycle through wallpapers in the same folder"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
}
Row {
id: controlsRow
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
StyledRect {
width: 60
height: 32
radius: Theme.cornerRadius
color: prevButtonArea.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.8) : Theme.primary
opacity: SessionData.wallpaperPath ? 1 : 0.5
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "skip_previous"
size: Theme.iconSizeSmall
color: Theme.primaryText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Prev"
color: Theme.primaryText
font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: prevButtonArea
anchors.fill: parent
enabled: SessionData.wallpaperPath
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
hoverEnabled: true
onClicked: {
WallpaperCyclingService.cyclePrevManually()
}
}
}
StyledRect {
width: 60
height: 32
radius: Theme.cornerRadius
color: nextButtonArea.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.8) : Theme.primary
opacity: SessionData.wallpaperPath ? 1 : 0.5
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "skip_next"
size: Theme.iconSizeSmall
color: Theme.primaryText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Next"
color: Theme.primaryText
font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: nextButtonArea
anchors.fill: parent
enabled: SessionData.wallpaperPath
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
hoverEnabled: true
onClicked: {
WallpaperCyclingService.cycleNextManually()
}
}
}
DankToggle {
id: cyclingToggle
anchors.verticalCenter: parent.verticalCenter
checked: SessionData.wallpaperCyclingEnabled
onToggled: toggled => {
return SessionData.setWallpaperCyclingEnabled(
toggled)
}
}
}
}
// Cycling mode and settings
Column {
width: parent.width
spacing: Theme.spacingS
visible: SessionData.wallpaperCyclingEnabled
leftPadding: Theme.iconSize + Theme.spacingM
Row {
spacing: Theme.spacingL
width: parent.width - parent.leftPadding
StyledText {
text: "Mode:"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
DankTabBar {
id: modeTabBar
width: 200
height: 32
model: [{
"text": "Interval"
}, {
"text": "Time"
}]
currentIndex: SessionData.wallpaperCyclingMode
=== "time" ? 1 : 0
onTabClicked: index => {
SessionData.setWallpaperCyclingMode(
index === 1 ? "time" : "interval")
}
}
}
// Interval settings
DankDropdown {
property var intervalOptions: ["1 minute", "5 minutes", "15 minutes", "30 minutes", "1 hour", "1.5 hours", "2 hours", "3 hours", "4 hours", "6 hours", "8 hours", "12 hours"]
property var intervalValues: [60, 300, 900, 1800, 3600, 5400, 7200, 10800, 14400, 21600, 28800, 43200]
width: parent.width - parent.leftPadding
visible: SessionData.wallpaperCyclingMode === "interval"
text: "Interval"
description: "How often to change wallpaper"
options: intervalOptions
currentValue: {
const currentSeconds = SessionData.wallpaperCyclingInterval
const index = intervalValues.indexOf(
currentSeconds)
return index >= 0 ? intervalOptions[index] : "5 minutes"
}
onValueChanged: value => {
const index = intervalOptions.indexOf(
value)
if (index >= 0)
SessionData.setWallpaperCyclingInterval(
intervalValues[index])
}
}
// Time settings
Row {
spacing: Theme.spacingM
visible: SessionData.wallpaperCyclingMode === "time"
width: parent.width - parent.leftPadding
StyledText {
text: "Daily at:"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
DankTextField {
width: 100
height: 40
text: SessionData.wallpaperCyclingTime
placeholderText: "00:00"
maximumLength: 5
topPadding: Theme.spacingS
bottomPadding: Theme.spacingS
onAccepted: {
var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(
text)
if (isValid)
SessionData.setWallpaperCyclingTime(
text)
else
text = SessionData.wallpaperCyclingTime
}
onEditingFinished: {
var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(
text)
if (isValid)
SessionData.setWallpaperCyclingTime(
text)
else
text = SessionData.wallpaperCyclingTime
}
anchors.verticalCenter: parent.verticalCenter
validator: RegularExpressionValidator {
regularExpression: /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/
}
}
StyledText {
text: "24-hour format"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
// Dynamic Theme Section
StyledRect {
width: parent.width
height: dynamicThemeSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: dynamicThemeSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "palette"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- toggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Dynamic Theming"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Automatically extract colors from wallpaper"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: toggle
anchors.verticalCenter: parent.verticalCenter
checked: Theme.currentTheme === Theme.dynamic
enabled: ToastService.wallpaperErrorStatus !== "matugen_missing"
onToggled: toggled => {
if (toggled)
Theme.switchTheme(Theme.dynamic)
else
Theme.switchTheme("blue")
}
}
}
StyledText {
text: "matugen not detected - dynamic theming unavailable"
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
visible: ToastService.wallpaperErrorStatus === "matugen_missing"
width: parent.width
leftPadding: Theme.iconSize + Theme.spacingM
}
}
}
// Display Settings
StyledRect {
width: parent.width
height: displaySection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: displaySection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "monitor"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Display Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
id: nightModeToggle
width: parent.width
text: "Night Mode"
description: "Apply warm color temperature to reduce eye strain"
checked: BrightnessService.nightModeActive
onToggled: checked => {
if (checked !== BrightnessService.nightModeActive) {
if (checked)
BrightnessService.enableNightMode()
else
BrightnessService.disableNightMode()
}
}
Connections {
function onNightModeActiveChanged() {
nightModeToggle.checked = BrightnessService.nightModeActive
}
target: BrightnessService
}
}
DankDropdown {
width: parent.width
text: "Night Mode Temperature"
description: BrightnessService.nightModeActive ? "Disable night mode to adjust" : "Set temperature for night mode"
enabled: !BrightnessService.nightModeActive
opacity: !BrightnessService.nightModeActive ? 1 : 0.6
currentValue: SessionData.nightModeTemperature + "K"
options: {
var temps = []
for (var i = 2500; i <= 6000; i += 500) {
temps.push(i + "K")
}
return temps
}
onValueChanged: value => {
var temp = parseInt(
value.replace("K", ""))
SessionData.setNightModeTemperature(
temp)
}
}
DankToggle {
width: parent.width
text: "Light Mode"
description: "Use light theme instead of dark theme"
checked: SessionData.isLightMode
onToggled: checked => {
SessionData.setLightMode(checked)
Theme.isLightMode = checked
PortalService.setLightMode(checked)
}
}
}
}
// Font Settings
StyledRect {
width: parent.width
height: fontSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: fontSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "font_download"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Font Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankDropdown {
width: parent.width
text: "Font Family"
description: "Select system font family"
currentValue: {
if (SettingsData.fontFamily === SettingsData.defaultFontFamily)
return "Default"
else
return SettingsData.fontFamily || "Default"
}
enableFuzzySearch: true
popupWidthOffset: 100
maxPopupHeight: 400
options: cachedFontFamilies
onValueChanged: value => {
if (value.startsWith("Default"))
SettingsData.setFontFamily(
SettingsData.defaultFontFamily)
else
SettingsData.setFontFamily(value)
}
}
DankDropdown {
width: parent.width
text: "Font Weight"
description: "Select font weight"
currentValue: {
switch (SettingsData.fontWeight) {
case Font.Thin:
return "Thin"
case Font.ExtraLight:
return "Extra Light"
case Font.Light:
return "Light"
case Font.Normal:
return "Regular"
case Font.Medium:
return "Medium"
case Font.DemiBold:
return "Demi Bold"
case Font.Bold:
return "Bold"
case Font.ExtraBold:
return "Extra Bold"
case Font.Black:
return "Black"
default:
return "Regular"
}
}
options: ["Thin", "Extra Light", "Light", "Regular", "Medium", "Demi Bold", "Bold", "Extra Bold", "Black"]
onValueChanged: value => {
var weight
switch (value) {
case "Thin":
weight = Font.Thin
break
case "Extra Light":
weight = Font.ExtraLight
break
case "Light":
weight = Font.Light
break
case "Regular":
weight = Font.Normal
break
case "Medium":
weight = Font.Medium
break
case "Demi Bold":
weight = Font.DemiBold
break
case "Bold":
weight = Font.Bold
break
case "Extra Bold":
weight = Font.ExtraBold
break
case "Black":
weight = Font.Black
break
default:
weight = Font.Normal
break
}
SettingsData.setFontWeight(weight)
}
}
DankDropdown {
width: parent.width
text: "Monospace Font"
description: "Select monospace font for process list and technical displays"
currentValue: {
if (SettingsData.monoFontFamily === SettingsData.defaultMonoFontFamily)
return "Default"
return SettingsData.monoFontFamily || "Default"
}
enableFuzzySearch: true
popupWidthOffset: 100
maxPopupHeight: 400
options: cachedMonoFamilies
onValueChanged: value => {
if (value === "Default")
SettingsData.setMonoFontFamily(
SettingsData.defaultMonoFontFamily)
else
SettingsData.setMonoFontFamily(
value)
}
}
}
}
// Corner Radius
StyledRect {
width: parent.width
height: cornerRadiusSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: cornerRadiusSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "rounded_corner"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Corner Radius"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: "Bar & Widget Corner Roundness"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width
height: 24
value: SettingsData.cornerRadius
minimum: 0
maximum: 32
unit: ""
showValue: true
onSliderValueChanged: newValue => {
SettingsData.setCornerRadius(
newValue)
}
}
}
}
}
}
}
FileBrowserModal {
id: wallpaperBrowser
browserTitle: "Select Wallpaper"
browserIcon: "wallpaper"
browserType: "wallpaper"
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: path => {
SessionData.setWallpaper(path)
close()
}
onDialogClosed: {
if (parentModal) {
parentModal.allowFocusOverride = false
parentModal.shouldHaveFocus = Qt.binding(() => {
return parentModal.shouldBeVisible
})
}
}
}
}
-251
View File
@@ -1,251 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell.Widgets
import qs.Common
import qs.Widgets
Item {
id: recentAppsTab
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
StyledRect {
width: parent.width
height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: recentlyUsedSection
property var rankedAppsModel: {
var apps = []
for (var appId in (AppUsageHistoryData.appUsageRanking
|| {})) {
var appData = (AppUsageHistoryData.appUsageRanking
|| {})[appId]
apps.push({
"id": appId,
"name": appData.name,
"exec": appData.exec,
"icon": appData.icon,
"comment": appData.comment,
"usageCount": appData.usageCount,
"lastUsed": appData.lastUsed
})
}
apps.sort(function (a, b) {
if (a.usageCount !== b.usageCount)
return b.usageCount - a.usageCount
return a.name.localeCompare(b.name)
})
return apps.slice(0, 20)
}
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "history"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Recently Used Apps"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - parent.children[0].width
- parent.children[1].width
- clearAllButton.width - Theme.spacingM * 3
height: 1
}
DankActionButton {
id: clearAllButton
iconName: "delete_sweep"
iconSize: Theme.iconSize - 2
iconColor: Theme.error
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g,
Theme.error.b, 0.12)
anchors.verticalCenter: parent.verticalCenter
onClicked: {
AppUsageHistoryData.appUsageRanking = {}
SettingsData.saveSettings()
}
}
}
StyledText {
width: parent.width
text: "Apps are ordered by usage frequency, then last used, then alphabetically."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
Column {
id: rankedAppsList
width: parent.width
spacing: Theme.spacingS
Repeater {
model: recentlyUsedSection.rankedAppsModel
delegate: Rectangle {
width: rankedAppsList.width
height: 48
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r,
Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.3)
border.color: Qt.rgba(Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.1)
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
StyledText {
text: (index + 1).toString()
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
width: 20
anchors.verticalCenter: parent.verticalCenter
}
Image {
width: 24
height: 24
source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable"
sourceSize.width: 24
sourceSize.height: 24
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
onStatusChanged: {
if (status === Image.Error)
source = "image://icon/application-x-executable"
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: modelData.name
|| "Unknown App"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: {
if (!modelData.lastUsed)
return "Never used"
var date = new Date(modelData.lastUsed)
var now = new Date()
var diffMs = now - date
var diffMins = Math.floor(
diffMs / (1000 * 60))
var diffHours = Math.floor(
diffMs / (1000 * 60 * 60))
var diffDays = Math.floor(
diffMs / (1000 * 60 * 60 * 24))
if (diffMins < 1)
return "Last launched just now"
if (diffMins < 60)
return "Last launched " + diffMins + " minute"
+ (diffMins === 1 ? "" : "s") + " ago"
if (diffHours < 24)
return "Last launched " + diffHours + " hour"
+ (diffHours === 1 ? "" : "s") + " ago"
if (diffDays < 7)
return "Last launched " + diffDays + " day"
+ (diffDays === 1 ? "" : "s") + " ago"
return "Last launched " + date.toLocaleDateString()
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
DankActionButton {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
circular: true
iconName: "close"
iconSize: 16
iconColor: Theme.error
hoverColor: Qt.rgba(Theme.error.r,
Theme.error.g,
Theme.error.b, 0.12)
onClicked: {
var currentRanking = Object.assign(
{},
AppUsageHistoryData.appUsageRanking
|| {})
delete currentRanking[modelData.id]
AppUsageHistoryData.appUsageRanking = currentRanking
SettingsData.saveSettings()
}
}
}
}
StyledText {
width: parent.width
text: recentlyUsedSection.rankedAppsModel.length
=== 0 ? "No apps have been launched yet." : ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
visible: recentlyUsedSection.rankedAppsModel.length === 0
}
}
}
}
}
}
}
-106
View File
@@ -1,106 +0,0 @@
import QtQuick
import qs.Common
import qs.Widgets
Column {
id: root
property string title: ""
property string iconName: ""
property alias content: contentLoader.sourceComponent
property bool expanded: false
property bool collapsible: true
property bool lazyLoad: true
width: parent.width
spacing: expanded ? Theme.spacingM : 0
Component.onCompleted: {
if (!collapsible)
expanded = true
}
MouseArea {
width: parent.width
height: headerRow.height
enabled: collapsible
hoverEnabled: collapsible
onClicked: {
if (collapsible)
expanded = !expanded
}
Rectangle {
anchors.fill: parent
color: parent.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.08) : "transparent"
radius: Theme.radiusS
}
Row {
id: headerRow
width: parent.width
spacing: Theme.spacingS
topPadding: Theme.spacingS
bottomPadding: Theme.spacingS
DankIcon {
name: root.collapsible ? (root.expanded ? "expand_less" : "expand_more") : root.iconName
size: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
Behavior on rotation {
NumberAnimation {
duration: Appearance.anim.durations.fast
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
DankIcon {
name: root.iconName
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: root.collapsible
}
StyledText {
text: root.title
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
visible: expanded || !collapsible
}
Loader {
id: contentLoader
width: parent.width
active: lazyLoad ? expanded || !collapsible : true
visible: expanded || !collapsible
asynchronous: true
opacity: visible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}
-937
View File
@@ -1,937 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell.Io
import qs.Common
import qs.Modals
import qs.Services
import qs.Widgets
Item {
id: themeColorsTab
property var cachedFontFamilies: []
property var cachedMonoFamilies: []
property bool fontsEnumerated: false
function enumerateFonts() {
var fonts = ["Default"]
var availableFonts = Qt.fontFamilies()
var rootFamilies = []
var seenFamilies = new Set()
for (var i = 0; i < availableFonts.length; i++) {
var fontName = availableFonts[i]
if (fontName.startsWith("."))
continue
if (fontName === SettingsData.defaultFontFamily)
continue
var rootName = fontName.replace(
/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i,
"").replace(
/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
"").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i,
function (match, suffix) {
return match
}).trim()
if (!seenFamilies.has(rootName) && rootName !== "") {
seenFamilies.add(rootName)
rootFamilies.push(rootName)
}
}
cachedFontFamilies = fonts.concat(rootFamilies.sort())
var monoFonts = ["Default"]
var monoFamilies = []
var seenMonoFamilies = new Set()
for (var j = 0; j < availableFonts.length; j++) {
var fontName2 = availableFonts[j]
if (fontName2.startsWith("."))
continue
if (fontName2 === SettingsData.defaultMonoFontFamily)
continue
var lowerName = fontName2.toLowerCase()
if (lowerName.includes("mono") || lowerName.includes(
"code") || lowerName.includes(
"console") || lowerName.includes(
"terminal") || lowerName.includes(
"courier") || lowerName.includes(
"dejavu sans mono") || lowerName.includes(
"jetbrains") || lowerName.includes(
"fira") || lowerName.includes(
"hack") || lowerName.includes(
"source code") || lowerName.includes(
"ubuntu mono") || lowerName.includes("cascadia")) {
var rootName2 = fontName2.replace(
/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i,
"").replace(
/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
"").trim()
if (!seenMonoFamilies.has(rootName2) && rootName2 !== "") {
seenMonoFamilies.add(rootName2)
monoFamilies.push(rootName2)
}
}
}
cachedMonoFamilies = monoFonts.concat(monoFamilies.sort())
}
Component.onCompleted: {
if (!fontsEnumerated) {
enumerateFonts()
fontsEnumerated = true
}
}
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
// Theme Color
StyledRect {
width: parent.width
height: themeSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: themeSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "palette"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Theme Color"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: "Current Theme: " + (Theme.currentTheme === Theme.dynamic ? "Dynamic" : Theme.getThemeColors(Theme.currentThemeName).name)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
if (Theme.currentTheme === Theme.dynamic)
return "Wallpaper-based dynamic colors"
var descriptions = {
"blue": "Material blue inspired by modern interfaces",
"deepBlue": "Deep blue inspired by material 3",
"purple": "Rich purple tones for elegance",
"green": "Natural green for productivity",
"orange": "Energetic orange for creativity",
"red": "Bold red for impact",
"cyan": "Cool cyan for tranquility",
"pink": "Vibrant pink for expression",
"amber": "Warm amber for comfort",
"coral": "Soft coral for gentle warmth"
}
return descriptions[Theme.currentThemeName] || "Select a theme"
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Text.WordWrap
width: Math.min(parent.width, 400)
horizontalAlignment: Text.AlignHCenter
}
}
Column {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: Theme.spacingM
Repeater {
model: Theme.availableThemeNames.slice(0, 5)
Rectangle {
property string themeName: modelData
width: 32
height: 32
radius: 16
color: Theme.getThemeColors(themeName).primary
border.color: Theme.outline
border.width: (Theme.currentThemeName === themeName
&& Theme.currentTheme !== Theme.dynamic) ? 2 : 1
scale: (Theme.currentThemeName === themeName
&& Theme.currentTheme !== Theme.dynamic) ? 1.1 : 1
Rectangle {
width: nameText.contentWidth + Theme.spacingS * 2
height: nameText.contentHeight + Theme.spacingXS * 2
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
radius: Theme.cornerRadius
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
visible: mouseArea.containsMouse
StyledText {
id: nameText
text: Theme.getThemeColors(themeName).name
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Theme.switchTheme(themeName)
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
Row {
spacing: Theme.spacingM
Repeater {
model: Theme.availableThemeNames.slice(5, 10)
Rectangle {
property string themeName: modelData
width: 32
height: 32
radius: 16
color: Theme.getThemeColors(themeName).primary
border.color: Theme.outline
border.width: Theme.currentThemeName === themeName ? 2 : 1
visible: true
scale: Theme.currentThemeName === themeName ? 1.1 : 1
Rectangle {
width: nameText2.contentWidth + Theme.spacingS * 2
height: nameText2.contentHeight + Theme.spacingXS * 2
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
radius: Theme.cornerRadius
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
visible: mouseArea2.containsMouse
StyledText {
id: nameText2
text: Theme.getThemeColors(themeName).name
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
MouseArea {
id: mouseArea2
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Theme.switchTheme(themeName)
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
Item {
width: 1
height: Theme.spacingM
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingL
Rectangle {
width: 120
height: 40
radius: 20
color: {
if (ToastService.wallpaperErrorStatus === "error"
|| ToastService.wallpaperErrorStatus === "matugen_missing")
return Qt.rgba(Theme.error.r,
Theme.error.g,
Theme.error.b, 0.12)
else
return Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
}
border.color: {
if (ToastService.wallpaperErrorStatus === "error"
|| ToastService.wallpaperErrorStatus === "matugen_missing")
return Qt.rgba(Theme.error.r,
Theme.error.g,
Theme.error.b, 0.5)
else if (Theme.currentThemeName === "dynamic")
return Theme.primary
else
return Theme.outline
}
border.width: (Theme.currentThemeName === "dynamic") ? 2 : 1
scale: (Theme.currentThemeName === "dynamic") ? 1.1 : (autoMouseArea.containsMouse ? 1.02 : 1)
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: {
if (ToastService.wallpaperErrorStatus === "error"
|| ToastService.wallpaperErrorStatus
=== "matugen_missing")
return "error"
else
return "palette"
}
size: 16
color: {
if (ToastService.wallpaperErrorStatus === "error"
|| ToastService.wallpaperErrorStatus
=== "matugen_missing")
return Theme.error
else
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (ToastService.wallpaperErrorStatus === "error")
return "Error"
else if (ToastService.wallpaperErrorStatus
=== "matugen_missing")
return "No matugen"
else
return "Auto"
}
font.pixelSize: Theme.fontSizeMedium
color: {
if (ToastService.wallpaperErrorStatus === "error"
|| ToastService.wallpaperErrorStatus
=== "matugen_missing")
return Theme.error
else
return Theme.surfaceText
}
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: autoMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (ToastService.wallpaperErrorStatus === "matugen_missing")
ToastService.showError(
"matugen not found - install matugen package for dynamic theming")
else if (ToastService.wallpaperErrorStatus === "error")
ToastService.showError(
"Wallpaper processing failed - check wallpaper path")
else
Theme.switchTheme(Theme.dynamic)
}
}
Rectangle {
width: autoTooltipText.contentWidth + Theme.spacingM * 2
height: autoTooltipText.contentHeight + Theme.spacingS * 2
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
radius: Theme.cornerRadius
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
visible: autoMouseArea.containsMouse
&& (Theme.currentTheme !== Theme.dynamic
|| ToastService.wallpaperErrorStatus === "error"
|| ToastService.wallpaperErrorStatus
=== "matugen_missing")
StyledText {
id: autoTooltipText
text: {
if (ToastService.wallpaperErrorStatus === "matugen_missing")
return "Install matugen package for dynamic themes"
else
return "Dynamic wallpaper-based colors"
}
font.pixelSize: Theme.fontSizeSmall
color: (ToastService.wallpaperErrorStatus === "error"
|| ToastService.wallpaperErrorStatus
=== "matugen_missing") ? Theme.error : Theme.surfaceText
anchors.centerIn: parent
wrapMode: Text.WordWrap
width: Math.min(implicitWidth, 250)
horizontalAlignment: Text.AlignHCenter
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: 120
height: 40
radius: 20
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: (Theme.currentThemeName === "custom") ? Theme.primary : Theme.outline
border.width: (Theme.currentThemeName === "custom") ? 2 : 1
scale: (Theme.currentThemeName === "custom") ? 1.1 : (customMouseArea.containsMouse ? 1.02 : 1)
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "folder_open"
size: 16
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Custom"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: customMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
fileBrowserModal.open()
}
}
Rectangle {
width: customTooltipText.contentWidth + Theme.spacingM * 2
height: customTooltipText.contentHeight + Theme.spacingS * 2
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
radius: Theme.cornerRadius
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
visible: customMouseArea.containsMouse
StyledText {
id: customTooltipText
text: {
if (Theme.currentThemeName === "custom")
return SettingsData.customThemeFile || "Custom theme loaded"
else
return "Load custom theme from JSON file"
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
wrapMode: Text.WordWrap
width: Math.min(implicitWidth, 250)
horizontalAlignment: Text.AlignHCenter
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
} // Close Row
}
}
}
// Transparency Settings
StyledRect {
width: parent.width
height: transparencySection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: transparencySection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "opacity"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Transparency Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: "Top Bar Transparency"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width
height: 24
value: Math.round(
SettingsData.topBarTransparency * 100)
minimum: 0
maximum: 100
unit: ""
showValue: true
onSliderValueChanged: newValue => {
SettingsData.setTopBarTransparency(
newValue / 100)
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: "Top Bar Widget Transparency"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width
height: 24
value: Math.round(
SettingsData.topBarWidgetTransparency * 100)
minimum: 0
maximum: 100
unit: ""
showValue: true
onSliderValueChanged: newValue => {
SettingsData.setTopBarWidgetTransparency(
newValue / 100)
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: "Popup Transparency"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width
height: 24
value: Math.round(
SettingsData.popupTransparency * 100)
minimum: 0
maximum: 100
unit: ""
showValue: true
onSliderValueChanged: newValue => {
SettingsData.setPopupTransparency(
newValue / 100)
}
}
}
}
}
// System Configuration Warning
Rectangle {
width: parent.width
height: warningText.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.warning.r, Theme.warning.g,
Theme.warning.b, 0.12)
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g,
Theme.warning.b, 0.3)
border.width: 1
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: "info"
size: Theme.iconSizeSmall
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: warningText
font.pixelSize: Theme.fontSizeSmall
text: "The below settings will modify your GTK and Qt settings. If you wish to preserve your current configurations, please back them up (qt5ct.conf|qt6ct.conf and ~/.config/gtk-3.0|gtk-4.0)."
wrapMode: Text.WordWrap
width: parent.width - Theme.iconSizeSmall - Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
}
}
}
// Icon Theme
StyledRect {
width: parent.width
height: iconThemeSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: iconThemeSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingXS
DankIcon {
name: "image"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
DankDropdown {
width: parent.width - Theme.iconSize - Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
text: "Icon Theme"
description: "DankShell & System Icons"
currentValue: SettingsData.iconTheme
enableFuzzySearch: true
popupWidthOffset: 100
maxPopupHeight: 236
options: {
SettingsData.detectAvailableIconThemes()
return SettingsData.availableIconThemes
}
onValueChanged: value => {
SettingsData.setIconTheme(value)
if (value !== "System Default"
&& !SettingsData.qt5ctAvailable
&& !SettingsData.qt6ctAvailable)
ToastService.showWarning(
"qt5ct or qt6ct not found - Qt app themes may not update without these tools")
}
}
}
}
}
// System App Theming
StyledRect {
width: parent.width
height: systemThemingSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
visible: Theme.matugenAvailable
Column {
id: systemThemingSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "extension"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "System App Theming"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
width: parent.width
spacing: Theme.spacingM
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 48
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
border.color: Theme.primary
border.width: 1
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "folder"
size: 16
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Apply GTK Colors"
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Theme.applyGtkColors()
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 48
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
border.color: Theme.primary
border.width: 1
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "settings"
size: 16
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Apply Qt Colors"
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Theme.applyQtColors()
}
}
}
StyledText {
text: `Generate baseline GTK3/4 or QT5/QT6 (requires qt6ct-kde) configurations to follow DMS colors. Only needed once.<br /><br />It is recommended to install <a href="https://github.com/AvengeMedia/DankMaterialShell/blob/master/README.md#Theming" style="text-decoration:none; color:${Theme.primary};">Colloid</a> GTK theme prior to applying GTK themes.`
textFormat: Text.RichText
linkColor: Theme.primary
onLinkActivated: url => Qt.openUrlExternally(url)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignHCenter
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
}
}
}
}
}
}
FileBrowserModal {
id: fileBrowserModal
browserTitle: "Select Custom Theme"
filterExtensions: ["*.json"]
showHiddenFiles: true
function selectCustomTheme() {
shouldBeVisible = true
}
onFileSelected: function(filePath) {
// Save the custom theme file path and switch to custom theme
if (filePath.endsWith(".json")) {
SettingsData.setCustomThemeFile(filePath)
Theme.switchTheme("custom")
close()
}
}
}
}
-380
View File
@@ -1,380 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
Item {
id: timeTab
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
// Time Format
StyledRect {
width: parent.width
height: timeSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: timeSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "schedule"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- toggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "24-Hour Format"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Use 24-hour time format instead of 12-hour AM/PM"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: toggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.use24HourClock
onToggled: checked => {
return SettingsData.setClockFormat(
checked)
}
}
}
}
}
// Date Format Section
StyledRect {
width: parent.width
height: dateSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: dateSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "calendar_today"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Date Format"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankDropdown {
width: parent.width
height: 50
text: "Top Bar Format"
description: "Preview: " + Qt.formatDate(
new Date(),
SettingsData.clockDateFormat)
currentValue: {
// Find matching preset or show "Custom"
const presets = [{
"format": "ddd d",
"label": "Day Date"
}, {
"format": "ddd MMM d",
"label": "Day Month Date"
}, {
"format": "MMM d",
"label": "Month Date"
}, {
"format": "M/d",
"label": "Numeric (M/D)"
}, {
"format": "d/M",
"label": "Numeric (D/M)"
}, {
"format": "ddd d MMM yyyy",
"label": "Full with Year"
}, {
"format": "yyyy-MM-dd",
"label": "ISO Date"
}, {
"format": "dddd, MMMM d",
"label": "Full Day & Month"
}]
const match = presets.find(p => {
return p.format
=== SettingsData.clockDateFormat
})
return match ? match.label : "Custom: " + SettingsData.clockDateFormat
}
options: ["Day Date", "Day Month Date", "Month Date", "Numeric (M/D)", "Numeric (D/M)", "Full with Year", "ISO Date", "Full Day & Month", "Custom..."]
onValueChanged: value => {
const formatMap = {
"Day Date": "ddd d",
"Day Month Date": "ddd MMM d",
"Month Date": "MMM d",
"Numeric (M/D)": "M/d",
"Numeric (D/M)": "d/M",
"Full with Year": "ddd d MMM yyyy",
"ISO Date": "yyyy-MM-dd",
"Full Day & Month": "dddd, MMMM d"
}
if (value === "Custom...") {
customFormatInput.visible = true
} else {
customFormatInput.visible = false
SettingsData.setClockDateFormat(
formatMap[value])
}
}
}
DankDropdown {
width: parent.width
height: 50
text: "Lock Screen Format"
description: "Preview: " + Qt.formatDate(
new Date(),
SettingsData.lockDateFormat)
currentValue: {
// Find matching preset or show "Custom"
const presets = [{
"format": "ddd d",
"label": "Day Date"
}, {
"format": "ddd MMM d",
"label": "Day Month Date"
}, {
"format": "MMM d",
"label": "Month Date"
}, {
"format": "M/d",
"label": "Numeric (M/D)"
}, {
"format": "d/M",
"label": "Numeric (D/M)"
}, {
"format": "ddd d MMM yyyy",
"label": "Full with Year"
}, {
"format": "yyyy-MM-dd",
"label": "ISO Date"
}, {
"format": "dddd, MMMM d",
"label": "Full Day & Month"
}]
const match = presets.find(p => {
return p.format
=== SettingsData.lockDateFormat
})
return match ? match.label : "Custom: " + SettingsData.lockDateFormat
}
options: ["Day Date", "Day Month Date", "Month Date", "Numeric (M/D)", "Numeric (D/M)", "Full with Year", "ISO Date", "Full Day & Month", "Custom..."]
onValueChanged: value => {
const formatMap = {
"Day Date": "ddd d",
"Day Month Date": "ddd MMM d",
"Month Date": "MMM d",
"Numeric (M/D)": "M/d",
"Numeric (D/M)": "d/M",
"Full with Year": "ddd d MMM yyyy",
"ISO Date": "yyyy-MM-dd",
"Full Day & Month": "dddd, MMMM d"
}
if (value === "Custom...") {
customLockFormatInput.visible = true
} else {
customLockFormatInput.visible = false
SettingsData.setLockDateFormat(
formatMap[value])
}
}
}
DankTextField {
id: customFormatInput
width: parent.width
visible: false
placeholderText: "Enter custom top bar format (e.g., ddd MMM d)"
text: SettingsData.clockDateFormat
onTextChanged: {
if (visible && text)
SettingsData.setClockDateFormat(text)
}
}
DankTextField {
id: customLockFormatInput
width: parent.width
visible: false
placeholderText: "Enter custom lock screen format (e.g., dddd, MMMM d)"
text: SettingsData.lockDateFormat
onTextChanged: {
if (visible && text)
SettingsData.setLockDateFormat(text)
}
}
Rectangle {
width: parent.width
height: formatHelp.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.2)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.1)
border.width: 1
Column {
id: formatHelp
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
StyledText {
text: "Format Legend"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
}
Row {
width: parent.width
spacing: Theme.spacingL
Column {
width: (parent.width - Theme.spacingL) / 2
spacing: 2
StyledText {
text: "• d - Day (1-31)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "• dd - Day (01-31)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "• ddd - Day name (Mon)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "• dddd - Day name (Monday)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "• M - Month (1-12)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Column {
width: (parent.width - Theme.spacingL) / 2
spacing: 2
StyledText {
text: "• MM - Month (01-12)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "• MMM - Month (Jan)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "• MMMM - Month (January)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "• yy - Year (24)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "• yyyy - Year (2024)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
}
}
}
}
}
}
-980
View File
@@ -1,980 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: topBarTab
property var baseWidgetDefinitions: [{
"id": "launcherButton",
"text": "App Launcher",
"description": "Quick access to application launcher",
"icon": "apps",
"enabled": true
}, {
"id": "workspaceSwitcher",
"text": "Workspace Switcher",
"description": "Shows current workspace and allows switching",
"icon": "view_module",
"enabled": true
}, {
"id": "focusedWindow",
"text": "Focused Window",
"description": "Display currently focused application title",
"icon": "window",
"enabled": true
}, {
"id": "runningApps",
"text": "Running Apps",
"description": "Shows all running applications with focus indication",
"icon": "apps",
"enabled": true
}, {
"id": "clock",
"text": "Clock",
"description": "Current time and date display",
"icon": "schedule",
"enabled": true
}, {
"id": "weather",
"text": "Weather Widget",
"description": "Current weather conditions and temperature",
"icon": "wb_sunny",
"enabled": true
}, {
"id": "music",
"text": "Media Controls",
"description": "Control currently playing media",
"icon": "music_note",
"enabled": true
}, {
"id": "clipboard",
"text": "Clipboard Manager",
"description": "Access clipboard history",
"icon": "content_paste",
"enabled": true
}, {
"id": "cpuUsage",
"text": "CPU Usage",
"description": "CPU usage indicator",
"icon": "memory",
"enabled": DgopService.dgopAvailable,
"warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined
}, {
"id": "memUsage",
"text": "Memory Usage",
"description": "Memory usage indicator",
"icon": "storage",
"enabled": DgopService.dgopAvailable,
"warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined
}, {
"id": "cpuTemp",
"text": "CPU Temperature",
"description": "CPU temperature display",
"icon": "device_thermostat",
"enabled": DgopService.dgopAvailable,
"warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined
}, {
"id": "gpuTemp",
"text": "GPU Temperature",
"description": "GPU temperature display",
"icon": "auto_awesome_mosaic",
"warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : "This widget prevents GPU power off states, which can significantly impact battery life on laptops. It is not recommended to use this on laptops with hybrid graphics.",
"enabled": DgopService.dgopAvailable
}, {
"id": "systemTray",
"text": "System Tray",
"description": "System notification area icons",
"icon": "notifications",
"enabled": true
}, {
"id": "privacyIndicator",
"text": "Privacy Indicator",
"description": "Shows when microphone, camera, or screen sharing is active",
"icon": "privacy_tip",
"enabled": true
}, {
"id": "controlCenterButton",
"text": "Control Center",
"description": "Access to system controls and settings",
"icon": "settings",
"enabled": true
}, {
"id": "notificationButton",
"text": "Notification Center",
"description": "Access to notifications and do not disturb",
"icon": "notifications",
"enabled": true
}, {
"id": "battery",
"text": "Battery",
"description": "Battery level and power management",
"icon": "battery_std",
"enabled": true
}, {
"id": "idleInhibitor",
"text": "Idle Inhibitor",
"description": "Prevent screen timeout",
"icon": "motion_sensor_active",
"enabled": true
}, {
"id": "spacer",
"text": "Spacer",
"description": "Customizable empty space",
"icon": "more_horiz",
"enabled": true
}, {
"id": "separator",
"text": "Separator",
"description": "Visual divider between widgets",
"icon": "remove",
"enabled": true
}]
property var defaultLeftWidgets: [{
"id": "launcherButton",
"enabled": true
}, {
"id": "workspaceSwitcher",
"enabled": true
}, {
"id": "focusedWindow",
"enabled": true
}]
property var defaultCenterWidgets: [{
"id": "music",
"enabled": true
}, {
"id": "clock",
"enabled": true
}, {
"id": "weather",
"enabled": true
}]
property var defaultRightWidgets: [{
"id": "privacyIndicator",
"enabled": true
}, {
"id": "systemTray",
"enabled": true
}, {
"id": "clipboard",
"enabled": true
}, {
"id": "notificationButton",
"enabled": true
}, {
"id": "battery",
"enabled": true
}, {
"id": "controlCenterButton",
"enabled": true
}]
function addWidgetToSection(widgetId, targetSection) {
var widgetObj = {
"id": widgetId,
"enabled": true
}
if (widgetId === "spacer")
widgetObj.size = 20
if (widgetId === "gpuTemp") {
widgetObj.selectedGpuIndex = 0
widgetObj.pciId = ""
}
if (widgetId === "controlCenterButton") {
widgetObj.showNetworkIcon = true
widgetObj.showBluetoothIcon = true
widgetObj.showAudioIcon = true
}
var widgets = []
if (targetSection === "left") {
widgets = SettingsData.topBarLeftWidgets.slice()
widgets.push(widgetObj)
SettingsData.setTopBarLeftWidgets(widgets)
} else if (targetSection === "center") {
widgets = SettingsData.topBarCenterWidgets.slice()
widgets.push(widgetObj)
SettingsData.setTopBarCenterWidgets(widgets)
} else if (targetSection === "right") {
widgets = SettingsData.topBarRightWidgets.slice()
widgets.push(widgetObj)
SettingsData.setTopBarRightWidgets(widgets)
}
}
function removeWidgetFromSection(sectionId, widgetIndex) {
var widgets = []
if (sectionId === "left") {
widgets = SettingsData.topBarLeftWidgets.slice()
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
widgets.splice(widgetIndex, 1)
}
SettingsData.setTopBarLeftWidgets(widgets)
} else if (sectionId === "center") {
widgets = SettingsData.topBarCenterWidgets.slice()
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
widgets.splice(widgetIndex, 1)
}
SettingsData.setTopBarCenterWidgets(widgets)
} else if (sectionId === "right") {
widgets = SettingsData.topBarRightWidgets.slice()
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
widgets.splice(widgetIndex, 1)
}
SettingsData.setTopBarRightWidgets(widgets)
}
}
function handleItemEnabledChanged(sectionId, itemId, enabled) {
var widgets = []
if (sectionId === "left")
widgets = SettingsData.topBarLeftWidgets.slice()
else if (sectionId === "center")
widgets = SettingsData.topBarCenterWidgets.slice()
else if (sectionId === "right")
widgets = SettingsData.topBarRightWidgets.slice()
for (var i = 0; i < widgets.length; i++) {
var widget = widgets[i]
var widgetId = typeof widget === "string" ? widget : widget.id
if (widgetId === itemId) {
if (typeof widget === "string") {
widgets[i] = {
"id": widget,
"enabled": enabled
}
} else {
var newWidget = {
"id": widget.id,
"enabled": enabled
}
if (widget.size !== undefined)
newWidget.size = widget.size
if (widget.selectedGpuIndex !== undefined)
newWidget.selectedGpuIndex = widget.selectedGpuIndex
else if (widget.id === "gpuTemp")
newWidget.selectedGpuIndex = 0
if (widget.pciId !== undefined)
newWidget.pciId = widget.pciId
else if (widget.id === "gpuTemp")
newWidget.pciId = ""
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon !== undefined ? widget.showNetworkIcon : true
newWidget.showBluetoothIcon = widget.showBluetoothIcon !== undefined ? widget.showBluetoothIcon : true
newWidget.showAudioIcon = widget.showAudioIcon !== undefined ? widget.showAudioIcon : true
}
widgets[i] = newWidget
}
break
}
}
if (sectionId === "left")
SettingsData.setTopBarLeftWidgets(widgets)
else if (sectionId === "center")
SettingsData.setTopBarCenterWidgets(widgets)
else if (sectionId === "right")
SettingsData.setTopBarRightWidgets(widgets)
}
function handleItemOrderChanged(sectionId, newOrder) {
if (sectionId === "left")
SettingsData.setTopBarLeftWidgets(newOrder)
else if (sectionId === "center")
SettingsData.setTopBarCenterWidgets(newOrder)
else if (sectionId === "right")
SettingsData.setTopBarRightWidgets(newOrder)
}
function handleSpacerSizeChanged(sectionId, itemId, newSize) {
var widgets = []
if (sectionId === "left")
widgets = SettingsData.topBarLeftWidgets.slice()
else if (sectionId === "center")
widgets = SettingsData.topBarCenterWidgets.slice()
else if (sectionId === "right")
widgets = SettingsData.topBarRightWidgets.slice()
for (var i = 0; i < widgets.length; i++) {
var widget = widgets[i]
var widgetId = typeof widget === "string" ? widget : widget.id
if (widgetId === itemId && widgetId === "spacer") {
if (typeof widget === "string") {
widgets[i] = {
"id": widget,
"enabled": true,
"size": newSize
}
} else {
var newWidget = {
"id": widget.id,
"enabled": widget.enabled,
"size": newSize
}
if (widget.selectedGpuIndex !== undefined)
newWidget.selectedGpuIndex = widget.selectedGpuIndex
if (widget.pciId !== undefined)
newWidget.pciId = widget.pciId
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon !== undefined ? widget.showNetworkIcon : true
newWidget.showBluetoothIcon = widget.showBluetoothIcon !== undefined ? widget.showBluetoothIcon : true
newWidget.showAudioIcon = widget.showAudioIcon !== undefined ? widget.showAudioIcon : true
}
widgets[i] = newWidget
}
break
}
}
if (sectionId === "left")
SettingsData.setTopBarLeftWidgets(widgets)
else if (sectionId === "center")
SettingsData.setTopBarCenterWidgets(widgets)
else if (sectionId === "right")
SettingsData.setTopBarRightWidgets(widgets)
}
function handleGpuSelectionChanged(sectionId, widgetIndex, selectedGpuIndex) {
var widgets = []
if (sectionId === "left")
widgets = SettingsData.topBarLeftWidgets.slice()
else if (sectionId === "center")
widgets = SettingsData.topBarCenterWidgets.slice()
else if (sectionId === "right")
widgets = SettingsData.topBarRightWidgets.slice()
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
var widget = widgets[widgetIndex]
if (typeof widget === "string") {
widgets[widgetIndex] = {
"id": widget,
"enabled": true,
"selectedGpuIndex": selectedGpuIndex,
"pciId": DgopService.availableGpus
&& DgopService.availableGpus.length
> selectedGpuIndex ? DgopService.availableGpus[selectedGpuIndex].pciId : ""
}
} else {
var newWidget = {
"id": widget.id,
"enabled": widget.enabled,
"selectedGpuIndex": selectedGpuIndex,
"pciId": DgopService.availableGpus
&& DgopService.availableGpus.length
> selectedGpuIndex ? DgopService.availableGpus[selectedGpuIndex].pciId : ""
}
if (widget.size !== undefined)
newWidget.size = widget.size
widgets[widgetIndex] = newWidget
}
}
if (sectionId === "left")
SettingsData.setTopBarLeftWidgets(widgets)
else if (sectionId === "center")
SettingsData.setTopBarCenterWidgets(widgets)
else if (sectionId === "right")
SettingsData.setTopBarRightWidgets(widgets)
}
function handleControlCenterSettingChanged(sectionId, widgetIndex, settingName, value) {
// Control Center settings are global, not per-widget instance
if (settingName === "showNetworkIcon") {
SettingsData.setControlCenterShowNetworkIcon(value)
} else if (settingName === "showBluetoothIcon") {
SettingsData.setControlCenterShowBluetoothIcon(value)
} else if (settingName === "showAudioIcon") {
SettingsData.setControlCenterShowAudioIcon(value)
}
}
function getItemsForSection(sectionId) {
var widgets = []
var widgetData = []
if (sectionId === "left")
widgetData = SettingsData.topBarLeftWidgets || []
else if (sectionId === "center")
widgetData = SettingsData.topBarCenterWidgets || []
else if (sectionId === "right")
widgetData = SettingsData.topBarRightWidgets || []
widgetData.forEach(widget => {
var widgetId = typeof widget === "string" ? widget : widget.id
var widgetEnabled = typeof widget
=== "string" ? true : widget.enabled
var widgetSize = typeof widget === "string" ? undefined : widget.size
var widgetSelectedGpuIndex = typeof widget
=== "string" ? undefined : widget.selectedGpuIndex
var widgetPciId = typeof widget
=== "string" ? undefined : widget.pciId
var widgetShowNetworkIcon = typeof widget === "string" ? undefined : widget.showNetworkIcon
var widgetShowBluetoothIcon = typeof widget === "string" ? undefined : widget.showBluetoothIcon
var widgetShowAudioIcon = typeof widget === "string" ? undefined : widget.showAudioIcon
var widgetDef = baseWidgetDefinitions.find(w => {
return w.id === widgetId
})
if (widgetDef) {
var item = Object.assign({}, widgetDef)
item.enabled = widgetEnabled
if (widgetSize !== undefined)
item.size = widgetSize
if (widgetSelectedGpuIndex !== undefined)
item.selectedGpuIndex = widgetSelectedGpuIndex
if (widgetPciId !== undefined)
item.pciId = widgetPciId
if (widgetShowNetworkIcon !== undefined)
item.showNetworkIcon = widgetShowNetworkIcon
if (widgetShowBluetoothIcon !== undefined)
item.showBluetoothIcon = widgetShowBluetoothIcon
if (widgetShowAudioIcon !== undefined)
item.showAudioIcon = widgetShowAudioIcon
widgets.push(item)
}
})
return widgets
}
Component.onCompleted: {
// Only set defaults if widgets have never been configured (null/undefined, not empty array)
if (!SettingsData.topBarLeftWidgets)
SettingsData.setTopBarLeftWidgets(defaultLeftWidgets)
if (!SettingsData.topBarCenterWidgets)
SettingsData.setTopBarCenterWidgets(defaultCenterWidgets)
if (!SettingsData.topBarRightWidgets)
SettingsData.setTopBarRightWidgets(defaultRightWidgets)
const sections = ["left", "center", "right"]
sections.forEach(sectionId => {
var widgets = []
if (sectionId === "left")
widgets = SettingsData.topBarLeftWidgets.slice()
else if (sectionId === "center")
widgets = SettingsData.topBarCenterWidgets.slice()
else if (sectionId === "right")
widgets = SettingsData.topBarRightWidgets.slice()
var updated = false
for (var i = 0; i < widgets.length; i++) {
var widget = widgets[i]
if (typeof widget === "object"
&& widget.id === "spacer"
&& !widget.size) {
widgets[i] = Object.assign({}, widget, {
"size": 20
})
updated = true
}
}
if (updated) {
if (sectionId === "left")
SettingsData.setTopBarLeftWidgets(widgets)
else if (sectionId === "center")
SettingsData.setTopBarCenterWidgets(widgets)
else if (sectionId === "right")
SettingsData.setTopBarRightWidgets(widgets)
}
})
}
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
anchors.bottomMargin: Theme.spacingS
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
// TopBar Auto-hide Section
StyledRect {
width: parent.width
height: topBarAutoHideSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: topBarAutoHideSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "visibility_off"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- autoHideToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Auto-hide"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Automatically hide the top bar to expand screen real estate"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: autoHideToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.topBarAutoHide
onToggled: toggled => {
return SettingsData.setTopBarAutoHide(
toggled)
}
}
}
}
}
// Spacing
StyledRect {
width: parent.width
height: topBarSpacingSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: topBarSpacingSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "space_bar"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Spacing"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: "Gap Around Top Bar (0 = edge-to-edge)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width
height: 24
value: SettingsData.topBarSpacing
minimum: 0
maximum: 32
unit: ""
showValue: true
onSliderValueChanged: newValue => {
SettingsData.setTopBarSpacing(
newValue)
}
}
}
DankToggle {
width: parent.width
text: "Square Corners"
description: "Disable corner radius for the top bar (always square corners)"
checked: SettingsData.topBarSquareCorners
onToggled: checked => {
SettingsData.setTopBarSquareCorners(
checked)
}
}
}
}
// Widget Management Section
StyledRect {
width: parent.width
height: widgetManagementSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: widgetManagementSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
RowLayout {
width: parent.width
spacing: Theme.spacingM
DankIcon {
id: widgetIcon
name: "widgets"
size: Theme.iconSize
color: Theme.primary
Layout.alignment: Qt.AlignVCenter
}
StyledText {
id: widgetTitle
text: "Widget Management"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
Layout.alignment: Qt.AlignVCenter
}
Item {
height: 1
Layout.fillWidth: true
}
Rectangle {
id: resetButton
width: 80
height: 28
radius: Theme.cornerRadius
color: resetArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariant
Layout.alignment: Qt.AlignVCenter
border.width: 1
border.color: resetArea.containsMouse ? Theme.outline : Qt.rgba(
Theme.outline.r,
Theme.outline.g,
Theme.outline.b,
0.5)
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "refresh"
size: 14
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Reset"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: resetArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
SettingsData.setTopBarLeftWidgets(
defaultLeftWidgets)
SettingsData.setTopBarCenterWidgets(
defaultCenterWidgets)
SettingsData.setTopBarRightWidgets(
defaultRightWidgets)
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
StyledText {
width: parent.width
text: "Drag widgets to reorder within sections. Use the eye icon to hide/show widgets (maintains spacing), or X to remove them completely."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
}
}
Column {
width: parent.width
spacing: Theme.spacingL
// Left Section
StyledRect {
width: parent.width
height: leftSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
WidgetsTabSection {
id: leftSection
anchors.fill: parent
anchors.margins: Theme.spacingL
title: "Left Section"
titleIcon: "format_align_left"
sectionId: "left"
allWidgets: topBarTab.baseWidgetDefinitions
items: topBarTab.getItemsForSection("left")
onItemEnabledChanged: (sectionId, itemId, enabled) => {
topBarTab.handleItemEnabledChanged(
sectionId,
itemId, enabled)
}
onItemOrderChanged: newOrder => {
topBarTab.handleItemOrderChanged(
"left", newOrder)
}
onAddWidget: sectionId => {
widgetSelectionPopup.allWidgets
= topBarTab.baseWidgetDefinitions
widgetSelectionPopup.targetSection = sectionId
widgetSelectionPopup.safeOpen()
}
onRemoveWidget: (sectionId, widgetIndex) => {
topBarTab.removeWidgetFromSection(
sectionId, widgetIndex)
}
onSpacerSizeChanged: (sectionId, itemId, newSize) => {
topBarTab.handleSpacerSizeChanged(
sectionId, itemId, newSize)
}
onCompactModeChanged: (widgetId, value) => {
if (widgetId === "clock") {
SettingsData.setClockCompactMode(
value)
} else if (widgetId === "music") {
SettingsData.setMediaSize(
value)
} else if (widgetId === "focusedWindow") {
SettingsData.setFocusedWindowCompactMode(
value)
} else if (widgetId === "runningApps") {
SettingsData.setRunningAppsCompactMode(
value)
}
}
onControlCenterSettingChanged: (sectionId, widgetIndex, settingName, value) => {
handleControlCenterSettingChanged(sectionId, widgetIndex, settingName, value)
}
onGpuSelectionChanged: (sectionId, widgetIndex, selectedIndex) => {
topBarTab.handleGpuSelectionChanged(
sectionId, widgetIndex,
selectedIndex)
}
}
}
// Center Section
StyledRect {
width: parent.width
height: centerSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
WidgetsTabSection {
id: centerSection
anchors.fill: parent
anchors.margins: Theme.spacingL
title: "Center Section"
titleIcon: "format_align_center"
sectionId: "center"
allWidgets: topBarTab.baseWidgetDefinitions
items: topBarTab.getItemsForSection("center")
onItemEnabledChanged: (sectionId, itemId, enabled) => {
topBarTab.handleItemEnabledChanged(
sectionId,
itemId, enabled)
}
onItemOrderChanged: newOrder => {
topBarTab.handleItemOrderChanged(
"center", newOrder)
}
onAddWidget: sectionId => {
widgetSelectionPopup.allWidgets
= topBarTab.baseWidgetDefinitions
widgetSelectionPopup.targetSection = sectionId
widgetSelectionPopup.safeOpen()
}
onRemoveWidget: (sectionId, widgetIndex) => {
topBarTab.removeWidgetFromSection(
sectionId, widgetIndex)
}
onSpacerSizeChanged: (sectionId, itemId, newSize) => {
topBarTab.handleSpacerSizeChanged(
sectionId, itemId, newSize)
}
onCompactModeChanged: (widgetId, value) => {
if (widgetId === "clock") {
SettingsData.setClockCompactMode(
value)
} else if (widgetId === "music") {
SettingsData.setMediaSize(
value)
} else if (widgetId === "focusedWindow") {
SettingsData.setFocusedWindowCompactMode(
value)
} else if (widgetId === "runningApps") {
SettingsData.setRunningAppsCompactMode(
value)
}
}
onControlCenterSettingChanged: (sectionId, widgetIndex, settingName, value) => {
handleControlCenterSettingChanged(sectionId, widgetIndex, settingName, value)
}
onGpuSelectionChanged: (sectionId, widgetIndex, selectedIndex) => {
topBarTab.handleGpuSelectionChanged(
sectionId, widgetIndex,
selectedIndex)
}
}
}
// Right Section
StyledRect {
width: parent.width
height: rightSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
WidgetsTabSection {
id: rightSection
anchors.fill: parent
anchors.margins: Theme.spacingL
title: "Right Section"
titleIcon: "format_align_right"
sectionId: "right"
allWidgets: topBarTab.baseWidgetDefinitions
items: topBarTab.getItemsForSection("right")
onItemEnabledChanged: (sectionId, itemId, enabled) => {
topBarTab.handleItemEnabledChanged(
sectionId,
itemId, enabled)
}
onItemOrderChanged: newOrder => {
topBarTab.handleItemOrderChanged(
"right", newOrder)
}
onAddWidget: sectionId => {
widgetSelectionPopup.allWidgets
= topBarTab.baseWidgetDefinitions
widgetSelectionPopup.targetSection = sectionId
widgetSelectionPopup.safeOpen()
}
onRemoveWidget: (sectionId, widgetIndex) => {
topBarTab.removeWidgetFromSection(
sectionId, widgetIndex)
}
onSpacerSizeChanged: (sectionId, itemId, newSize) => {
topBarTab.handleSpacerSizeChanged(
sectionId, itemId, newSize)
}
onCompactModeChanged: (widgetId, value) => {
if (widgetId === "clock") {
SettingsData.setClockCompactMode(
value)
} else if (widgetId === "music") {
SettingsData.setMediaSize(
value)
} else if (widgetId === "focusedWindow") {
SettingsData.setFocusedWindowCompactMode(
value)
} else if (widgetId === "runningApps") {
SettingsData.setRunningAppsCompactMode(
value)
}
}
onControlCenterSettingChanged: (sectionId, widgetIndex, settingName, value) => {
handleControlCenterSettingChanged(sectionId, widgetIndex, settingName, value)
}
onGpuSelectionChanged: (sectionId, widgetIndex, selectedIndex) => {
topBarTab.handleGpuSelectionChanged(
sectionId, widgetIndex,
selectedIndex)
}
}
}
}
}
}
DankWidgetSelectionPopup {
id: widgetSelectionPopup
anchors.centerIn: parent
onWidgetSelected: (widgetId, targetSection) => {
topBarTab.addWidgetToSection(widgetId,
targetSection)
}
}
}
-267
View File
@@ -1,267 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
Item {
id: weatherTab
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
// Enable Weather
StyledRect {
width: parent.width
height: enableWeatherSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: enableWeatherSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "cloud"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- enableToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Enable Weather"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Show weather information in top bar and control center"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: enableToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.weatherEnabled
onToggled: checked => {
return SettingsData.setWeatherEnabled(
checked)
}
}
}
}
}
// Temperature Unit
StyledRect {
width: parent.width
height: temperatureSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
visible: SettingsData.weatherEnabled
opacity: visible ? 1 : 0
Column {
id: temperatureSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "thermostat"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- temperatureToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Use Fahrenheit"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Use Fahrenheit instead of Celsius for temperature"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: temperatureToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.useFahrenheit
onToggled: checked => {
return SettingsData.setTemperatureUnit(
checked)
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
// Location Settings
StyledRect {
width: parent.width
height: locationSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
visible: SettingsData.weatherEnabled
opacity: visible ? 1 : 0
Column {
id: locationSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "location_on"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- autoLocationToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Auto Location"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Allow wttr.in to determine location based on IP address"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: autoLocationToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.useAutoLocation
onToggled: checked => {
return SettingsData.setAutoLocation(
checked)
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: !SettingsData.useAutoLocation
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.2
}
StyledText {
text: "Custom Location"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
DankLocationSearch {
width: parent.width
currentLocation: SettingsData.weatherLocation
placeholderText: "New York, NY"
onLocationSelected: (displayName, coordinates) => {
SettingsData.setWeatherLocation(
displayName,
coordinates)
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}
-395
View File
@@ -1,395 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: widgetTweaksTab
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
// Launcher Button Section
StyledRect {
width: parent.width
height: launcherButtonSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: launcherButtonSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "apps"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Launcher Button"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: "Use OS Logo"
description: "Display operating system logo instead of apps icon"
checked: SettingsData.useOSLogo
onToggled: checked => {
return SettingsData.setUseOSLogo(checked)
}
}
Row {
width: parent.width - Theme.spacingL
spacing: Theme.spacingL
visible: SettingsData.useOSLogo
opacity: visible ? 1 : 0
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
Column {
width: 120
spacing: Theme.spacingS
StyledText {
text: "Color Override"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
width: 100
height: 28
placeholderText: "#ffffff"
text: SettingsData.osLogoColorOverride
maximumLength: 7
font.pixelSize: Theme.fontSizeSmall
topPadding: Theme.spacingXS
bottomPadding: Theme.spacingXS
onEditingFinished: {
var color = text.trim()
if (color === ""
|| /^#[0-9A-Fa-f]{6}$/.test(color))
SettingsData.setOSLogoColorOverride(
color)
else
text = SettingsData.osLogoColorOverride
}
}
}
Column {
width: 120
spacing: Theme.spacingS
StyledText {
text: "Brightness"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: 100
height: 20
minimum: 0
maximum: 100
value: Math.round(
SettingsData.osLogoBrightness * 100)
unit: "%"
showValue: true
onSliderValueChanged: newValue => {
SettingsData.setOSLogoBrightness(
newValue / 100)
}
}
}
Column {
width: 120
spacing: Theme.spacingS
StyledText {
text: "Contrast"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: 100
height: 20
minimum: 0
maximum: 200
value: Math.round(
SettingsData.osLogoContrast * 100)
unit: "%"
showValue: true
onSliderValueChanged: newValue => {
SettingsData.setOSLogoContrast(
newValue / 100)
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
StyledRect {
width: parent.width
height: workspaceSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: workspaceSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "view_module"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Workspace Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: "Workspace Index Numbers"
description: "Show workspace index numbers in the top bar workspace switcher"
checked: SettingsData.showWorkspaceIndex
onToggled: checked => {
return SettingsData.setShowWorkspaceIndex(
checked)
}
}
DankToggle {
width: parent.width
text: "Workspace Padding"
description: "Always show a minimum of 3 workspaces, even if fewer are available"
checked: SettingsData.showWorkspacePadding
onToggled: checked => {
return SettingsData.setShowWorkspacePadding(
checked)
}
}
}
}
StyledRect {
width: parent.width
height: workspaceIconsSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
visible: SettingsData.hasNamedWorkspaces()
Column {
id: workspaceIconsSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "label"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Named Workspace Icons"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
width: parent.width
text: "Configure icons for named workspaces. Icons take priority over numbers when both are enabled."
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
wrapMode: Text.WordWrap
}
Repeater {
model: SettingsData.getNamedWorkspaces()
Rectangle {
width: parent.width
height: workspaceIconRow.implicitHeight + Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r,
Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.5)
border.color: Qt.rgba(Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.3)
border.width: 1
Row {
id: workspaceIconRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
StyledText {
text: "\"" + modelData + "\""
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
width: 150
elide: Text.ElideRight
}
DankIconPicker {
id: iconPicker
anchors.verticalCenter: parent.verticalCenter
Component.onCompleted: {
var iconData = SettingsData.getWorkspaceNameIcon(
modelData)
if (iconData) {
setIcon(iconData.value,
iconData.type)
}
}
onIconSelected: (iconName, iconType) => {
SettingsData.setWorkspaceNameIcon(
modelData, {
"type": iconType,
"value": iconName
})
setIcon(iconName,
iconType)
}
Connections {
target: SettingsData
function onWorkspaceIconsUpdated() {
var iconData = SettingsData.getWorkspaceNameIcon(
modelData)
if (iconData) {
iconPicker.setIcon(
iconData.value,
iconData.type)
} else {
iconPicker.setIcon("", "icon")
}
}
}
}
Rectangle {
width: 28
height: 28
radius: Theme.cornerRadius
color: clearMouseArea.containsMouse ? Theme.errorHover : Theme.surfaceContainer
border.color: clearMouseArea.containsMouse ? Theme.error : Theme.outline
border.width: 1
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "close"
size: 16
color: clearMouseArea.containsMouse ? Theme.error : Theme.outline
anchors.centerIn: parent
}
MouseArea {
id: clearMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
SettingsData.removeWorkspaceNameIcon(
modelData)
}
}
}
Item {
width: parent.width - 150 - 240 - 28 - Theme.spacingM * 4
height: 1
}
}
}
}
}
}
}
}
}
-954
View File
@@ -1,954 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: widgetsTab
property var baseWidgetDefinitions: [{
"id": "launcherButton",
"text": "App Launcher",
"description": "Quick access to application launcher",
"icon": "apps",
"enabled": true
}, {
"id": "workspaceSwitcher",
"text": "Workspace Switcher",
"description": "Shows current workspace and allows switching",
"icon": "view_module",
"enabled": true
}, {
"id": "focusedWindow",
"text": "Focused Window",
"description": "Display currently focused application title",
"icon": "window",
"enabled": true
}, {
"id": "runningApps",
"text": "Running Apps",
"description": "Shows all running applications with focus indication",
"icon": "apps",
"enabled": true
}, {
"id": "clock",
"text": "Clock",
"description": "Current time and date display",
"icon": "schedule",
"enabled": true
}, {
"id": "weather",
"text": "Weather Widget",
"description": "Current weather conditions and temperature",
"icon": "wb_sunny",
"enabled": true
}, {
"id": "music",
"text": "Media Controls",
"description": "Control currently playing media",
"icon": "music_note",
"enabled": true
}, {
"id": "clipboard",
"text": "Clipboard Manager",
"description": "Access clipboard history",
"icon": "content_paste",
"enabled": true
}, {
"id": "cpuUsage",
"text": "CPU Usage",
"description": "CPU usage indicator",
"icon": "memory",
"enabled": DgopService.dgopAvailable,
"warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined
}, {
"id": "memUsage",
"text": "Memory Usage",
"description": "Memory usage indicator",
"icon": "storage",
"enabled": DgopService.dgopAvailable,
"warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined
}, {
"id": "cpuTemp",
"text": "CPU Temperature",
"description": "CPU temperature display",
"icon": "device_thermostat",
"enabled": DgopService.dgopAvailable,
"warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined
}, {
"id": "gpuTemp",
"text": "GPU Temperature",
"description": "GPU temperature display",
"icon": "auto_awesome_mosaic",
"warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : "This widget prevents GPU power off states, which can significantly impact battery life on laptops. It is not recommended to use this on laptops with hybrid graphics.",
"enabled": DgopService.dgopAvailable
}, {
"id": "systemTray",
"text": "System Tray",
"description": "System notification area icons",
"icon": "notifications",
"enabled": true
}, {
"id": "privacyIndicator",
"text": "Privacy Indicator",
"description": "Shows when microphone, camera, or screen sharing is active",
"icon": "privacy_tip",
"enabled": true
}, {
"id": "controlCenterButton",
"text": "Control Center",
"description": "Access to system controls and settings",
"icon": "settings",
"enabled": true
}, {
"id": "notificationButton",
"text": "Notification Center",
"description": "Access to notifications and do not disturb",
"icon": "notifications",
"enabled": true
}, {
"id": "battery",
"text": "Battery",
"description": "Battery level and power management",
"icon": "battery_std",
"enabled": true
}, {
"id": "idleInhibitor",
"text": "Idle Inhibitor",
"description": "Prevent screen timeout",
"icon": "motion_sensor_active",
"enabled": true
}, {
"id": "spacer",
"text": "Spacer",
"description": "Customizable empty space",
"icon": "more_horiz",
"enabled": true
}, {
"id": "separator",
"text": "Separator",
"description": "Visual divider between widgets",
"icon": "remove",
"enabled": true
}]
property var defaultLeftWidgets: [{
"id": "launcherButton",
"enabled": true
}, {
"id": "workspaceSwitcher",
"enabled": true
}, {
"id": "focusedWindow",
"enabled": true
}]
property var defaultCenterWidgets: [{
"id": "music",
"enabled": true
}, {
"id": "clock",
"enabled": true
}, {
"id": "weather",
"enabled": true
}]
property var defaultRightWidgets: [{
"id": "privacyIndicator",
"enabled": true
}, {
"id": "systemTray",
"enabled": true
}, {
"id": "clipboard",
"enabled": true
}, {
"id": "notificationButton",
"enabled": true
}, {
"id": "battery",
"enabled": true
}, {
"id": "controlCenterButton",
"enabled": true
}]
function addWidgetToSection(widgetId, targetSection) {
var widgetObj = {
"id": widgetId,
"enabled": true
}
if (widgetId === "spacer")
widgetObj.size = 20
if (widgetId === "gpuTemp") {
widgetObj.selectedGpuIndex = 0
widgetObj.pciId = ""
}
var widgets = []
if (targetSection === "left") {
widgets = SettingsData.topBarLeftWidgets.slice()
widgets.push(widgetObj)
SettingsData.setTopBarLeftWidgets(widgets)
} else if (targetSection === "center") {
widgets = SettingsData.topBarCenterWidgets.slice()
widgets.push(widgetObj)
SettingsData.setTopBarCenterWidgets(widgets)
} else if (targetSection === "right") {
widgets = SettingsData.topBarRightWidgets.slice()
widgets.push(widgetObj)
SettingsData.setTopBarRightWidgets(widgets)
}
}
function removeWidgetFromSection(sectionId, widgetIndex) {
var widgets = []
if (sectionId === "left") {
widgets = SettingsData.topBarLeftWidgets.slice()
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
widgets.splice(widgetIndex, 1)
}
SettingsData.setTopBarLeftWidgets(widgets)
} else if (sectionId === "center") {
widgets = SettingsData.topBarCenterWidgets.slice()
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
widgets.splice(widgetIndex, 1)
}
SettingsData.setTopBarCenterWidgets(widgets)
} else if (sectionId === "right") {
widgets = SettingsData.topBarRightWidgets.slice()
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
widgets.splice(widgetIndex, 1)
}
SettingsData.setTopBarRightWidgets(widgets)
}
}
function handleItemEnabledChanged(sectionId, itemId, enabled) {
var widgets = []
if (sectionId === "left")
widgets = SettingsData.topBarLeftWidgets.slice()
else if (sectionId === "center")
widgets = SettingsData.topBarCenterWidgets.slice()
else if (sectionId === "right")
widgets = SettingsData.topBarRightWidgets.slice()
for (var i = 0; i < widgets.length; i++) {
var widget = widgets[i]
var widgetId = typeof widget === "string" ? widget : widget.id
if (widgetId === itemId) {
if (typeof widget === "string") {
widgets[i] = {
"id": widget,
"enabled": enabled
}
} else {
var newWidget = {
"id": widget.id,
"enabled": enabled
}
if (widget.size !== undefined)
newWidget.size = widget.size
if (widget.selectedGpuIndex !== undefined)
newWidget.selectedGpuIndex = widget.selectedGpuIndex
else if (widget.id === "gpuTemp")
newWidget.selectedGpuIndex = 0
if (widget.pciId !== undefined)
newWidget.pciId = widget.pciId
else if (widget.id === "gpuTemp")
newWidget.pciId = ""
widgets[i] = newWidget
}
break
}
}
if (sectionId === "left")
SettingsData.setTopBarLeftWidgets(widgets)
else if (sectionId === "center")
SettingsData.setTopBarCenterWidgets(widgets)
else if (sectionId === "right")
SettingsData.setTopBarRightWidgets(widgets)
}
function handleItemOrderChanged(sectionId, newOrder) {
if (sectionId === "left")
SettingsData.setTopBarLeftWidgets(newOrder)
else if (sectionId === "center")
SettingsData.setTopBarCenterWidgets(newOrder)
else if (sectionId === "right")
SettingsData.setTopBarRightWidgets(newOrder)
}
function handleSpacerSizeChanged(sectionId, itemId, newSize) {
var widgets = []
if (sectionId === "left")
widgets = SettingsData.topBarLeftWidgets.slice()
else if (sectionId === "center")
widgets = SettingsData.topBarCenterWidgets.slice()
else if (sectionId === "right")
widgets = SettingsData.topBarRightWidgets.slice()
for (var i = 0; i < widgets.length; i++) {
var widget = widgets[i]
var widgetId = typeof widget === "string" ? widget : widget.id
if (widgetId === itemId && widgetId === "spacer") {
if (typeof widget === "string") {
widgets[i] = {
"id": widget,
"enabled": true,
"size": newSize
}
} else {
var newWidget = {
"id": widget.id,
"enabled": widget.enabled,
"size": newSize
}
if (widget.selectedGpuIndex !== undefined)
newWidget.selectedGpuIndex = widget.selectedGpuIndex
if (widget.pciId !== undefined)
newWidget.pciId = widget.pciId
widgets[i] = newWidget
}
break
}
}
if (sectionId === "left")
SettingsData.setTopBarLeftWidgets(widgets)
else if (sectionId === "center")
SettingsData.setTopBarCenterWidgets(widgets)
else if (sectionId === "right")
SettingsData.setTopBarRightWidgets(widgets)
}
function handleGpuSelectionChanged(sectionId, widgetIndex, selectedGpuIndex) {
var widgets = []
if (sectionId === "left")
widgets = SettingsData.topBarLeftWidgets.slice()
else if (sectionId === "center")
widgets = SettingsData.topBarCenterWidgets.slice()
else if (sectionId === "right")
widgets = SettingsData.topBarRightWidgets.slice()
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
var widget = widgets[widgetIndex]
if (typeof widget === "string") {
widgets[widgetIndex] = {
"id": widget,
"enabled": true,
"selectedGpuIndex": selectedGpuIndex,
"pciId": DgopService.availableGpus
&& DgopService.availableGpus.length
> selectedGpuIndex ? DgopService.availableGpus[selectedGpuIndex].pciId : ""
}
} else {
var newWidget = {
"id": widget.id,
"enabled": widget.enabled,
"selectedGpuIndex": selectedGpuIndex,
"pciId": DgopService.availableGpus
&& DgopService.availableGpus.length
> selectedGpuIndex ? DgopService.availableGpus[selectedGpuIndex].pciId : ""
}
if (widget.size !== undefined)
newWidget.size = widget.size
widgets[widgetIndex] = newWidget
}
}
if (sectionId === "left")
SettingsData.setTopBarLeftWidgets(widgets)
else if (sectionId === "center")
SettingsData.setTopBarCenterWidgets(widgets)
else if (sectionId === "right")
SettingsData.setTopBarRightWidgets(widgets)
}
function getItemsForSection(sectionId) {
var widgets = []
var widgetData = []
if (sectionId === "left")
widgetData = SettingsData.topBarLeftWidgets || []
else if (sectionId === "center")
widgetData = SettingsData.topBarCenterWidgets || []
else if (sectionId === "right")
widgetData = SettingsData.topBarRightWidgets || []
widgetData.forEach(widget => {
var widgetId = typeof widget === "string" ? widget : widget.id
var widgetEnabled = typeof widget
=== "string" ? true : widget.enabled
var widgetSize = typeof widget === "string" ? undefined : widget.size
var widgetSelectedGpuIndex = typeof widget
=== "string" ? undefined : widget.selectedGpuIndex
var widgetPciId = typeof widget
=== "string" ? undefined : widget.pciId
var widgetDef = baseWidgetDefinitions.find(w => {
return w.id === widgetId
})
if (widgetDef) {
var item = Object.assign({}, widgetDef)
item.enabled = widgetEnabled
if (widgetSize !== undefined)
item.size = widgetSize
if (widgetSelectedGpuIndex !== undefined)
item.selectedGpuIndex = widgetSelectedGpuIndex
if (widgetPciId !== undefined)
item.pciId = widgetPciId
widgets.push(item)
}
})
return widgets
}
Component.onCompleted: {
// Only set defaults if widgets have never been configured (null/undefined, not empty array)
if (!SettingsData.topBarLeftWidgets)
SettingsData.setTopBarLeftWidgets(defaultLeftWidgets)
if (!SettingsData.topBarCenterWidgets)
SettingsData.setTopBarCenterWidgets(defaultCenterWidgets)
if (!SettingsData.topBarRightWidgets)
SettingsData.setTopBarRightWidgets(
defaultRightWidgets)["left""center""right"].forEach(
sectionId => {
var widgets = []
if (sectionId === "left")
widgets = SettingsData.topBarLeftWidgets.slice()
else if (sectionId === "center")
widgets = SettingsData.topBarCenterWidgets.slice()
else if (sectionId === "right")
widgets = SettingsData.topBarRightWidgets.slice()
var updated = false
for (var i = 0; i < widgets.length; i++) {
var widget = widgets[i]
if (typeof widget === "object"
&& widget.id === "spacer" && !widget.size) {
widgets[i] = Object.assign({}, widget, {
"size": 20
})
updated = true
}
}
if (updated) {
if (sectionId === "left")
SettingsData.setTopBarLeftWidgets(widgets)
else if (sectionId === "center")
SettingsData.setTopBarCenterWidgets(widgets)
else if (sectionId === "right")
SettingsData.setTopBarRightWidgets(widgets)
}
})
}
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "widgets"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Top Bar Widget Management"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 400
height: 1
}
Rectangle {
width: 80
height: 28
radius: Theme.cornerRadius
color: resetArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariant
anchors.verticalCenter: parent.verticalCenter
border.width: 1
border.color: resetArea.containsMouse ? Theme.outline : Qt.rgba(
Theme.outline.r,
Theme.outline.g,
Theme.outline.b,
0.5)
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "refresh"
size: 14
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Reset"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: resetArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
SettingsData.setTopBarLeftWidgets(
defaultLeftWidgets)
SettingsData.setTopBarCenterWidgets(
defaultCenterWidgets)
SettingsData.setTopBarRightWidgets(
defaultRightWidgets)
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Rectangle {
width: parent.width
height: messageText.contentHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
StyledText {
id: messageText
anchors.centerIn: parent
text: "Drag widgets to reorder within sections. Use the eye icon to hide/show widgets (maintains spacing), or X to remove them completely."
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
width: parent.width - Theme.spacingM * 2
wrapMode: Text.WordWrap
}
}
Column {
width: parent.width
spacing: Theme.spacingL
WidgetsTabSection {
width: parent.width
title: "Left Section"
titleIcon: "format_align_left"
sectionId: "left"
allWidgets: widgetsTab.baseWidgetDefinitions
items: widgetsTab.getItemsForSection("left")
onItemEnabledChanged: (sectionId, itemId, enabled) => {
widgetsTab.handleItemEnabledChanged(
sectionId, itemId, enabled)
}
onItemOrderChanged: newOrder => {
widgetsTab.handleItemOrderChanged(
"left", newOrder)
}
onAddWidget: sectionId => {
widgetSelectionPopup.allWidgets
= widgetsTab.baseWidgetDefinitions
widgetSelectionPopup.targetSection = sectionId
widgetSelectionPopup.safeOpen()
}
onRemoveWidget: (sectionId, widgetIndex) => {
widgetsTab.removeWidgetFromSection(
sectionId, widgetIndex)
}
onSpacerSizeChanged: (sectionId, itemId, newSize) => {
widgetsTab.handleSpacerSizeChanged(
sectionId, itemId, newSize)
}
onCompactModeChanged: (widgetId, value) => {
if (widgetId === "clock") {
SettingsData.setClockCompactMode(
value)
} else if (widgetId === "music") {
SettingsData.setMediaSize(
value)
} else if (widgetId === "focusedWindow") {
SettingsData.setFocusedWindowCompactMode(
value)
} else if (widgetId === "runningApps") {
SettingsData.setRunningAppsCompactMode(
value)
}
}
onGpuSelectionChanged: (sectionId, widgetIndex, selectedIndex) => {
widgetsTab.handleGpuSelectionChanged(
sectionId, widgetIndex,
selectedIndex)
}
}
WidgetsTabSection {
width: parent.width
title: "Center Section"
titleIcon: "format_align_center"
sectionId: "center"
allWidgets: widgetsTab.baseWidgetDefinitions
items: widgetsTab.getItemsForSection("center")
onItemEnabledChanged: (sectionId, itemId, enabled) => {
widgetsTab.handleItemEnabledChanged(
sectionId, itemId, enabled)
}
onItemOrderChanged: newOrder => {
widgetsTab.handleItemOrderChanged(
"center", newOrder)
}
onAddWidget: sectionId => {
widgetSelectionPopup.allWidgets
= widgetsTab.baseWidgetDefinitions
widgetSelectionPopup.targetSection = sectionId
widgetSelectionPopup.safeOpen()
}
onRemoveWidget: (sectionId, widgetIndex) => {
widgetsTab.removeWidgetFromSection(
sectionId, widgetIndex)
}
onSpacerSizeChanged: (sectionId, itemId, newSize) => {
widgetsTab.handleSpacerSizeChanged(
sectionId, itemId, newSize)
}
onCompactModeChanged: (widgetId, value) => {
if (widgetId === "clock") {
SettingsData.setClockCompactMode(
value)
} else if (widgetId === "music") {
SettingsData.setMediaSize(
value)
} else if (widgetId === "focusedWindow") {
SettingsData.setFocusedWindowCompactMode(
value)
} else if (widgetId === "runningApps") {
SettingsData.setRunningAppsCompactMode(
value)
}
}
onGpuSelectionChanged: (sectionId, widgetIndex, selectedIndex) => {
widgetsTab.handleGpuSelectionChanged(
sectionId, widgetIndex,
selectedIndex)
}
}
WidgetsTabSection {
width: parent.width
title: "Right Section"
titleIcon: "format_align_right"
sectionId: "right"
allWidgets: widgetsTab.baseWidgetDefinitions
items: widgetsTab.getItemsForSection("right")
onItemEnabledChanged: (sectionId, itemId, enabled) => {
widgetsTab.handleItemEnabledChanged(
sectionId, itemId, enabled)
}
onItemOrderChanged: newOrder => {
widgetsTab.handleItemOrderChanged(
"right", newOrder)
}
onAddWidget: sectionId => {
widgetSelectionPopup.allWidgets
= widgetsTab.baseWidgetDefinitions
widgetSelectionPopup.targetSection = sectionId
widgetSelectionPopup.safeOpen()
}
onRemoveWidget: (sectionId, widgetIndex) => {
widgetsTab.removeWidgetFromSection(
sectionId, widgetIndex)
}
onSpacerSizeChanged: (sectionId, itemId, newSize) => {
widgetsTab.handleSpacerSizeChanged(
sectionId, itemId, newSize)
}
onCompactModeChanged: (widgetId, value) => {
if (widgetId === "clock") {
SettingsData.setClockCompactMode(
value)
} else if (widgetId === "music") {
SettingsData.setMediaSize(
value)
} else if (widgetId === "focusedWindow") {
SettingsData.setFocusedWindowCompactMode(
value)
} else if (widgetId === "runningApps") {
SettingsData.setRunningAppsCompactMode(
value)
}
}
onGpuSelectionChanged: (sectionId, widgetIndex, selectedIndex) => {
widgetsTab.handleGpuSelectionChanged(
sectionId, widgetIndex,
selectedIndex)
}
}
}
StyledRect {
width: parent.width
height: workspaceSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: workspaceSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "view_module"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Workspace Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: "Workspace Index Numbers"
description: "Show workspace index numbers in the top bar workspace switcher"
checked: SettingsData.showWorkspaceIndex
onToggled: checked => {
return SettingsData.setShowWorkspaceIndex(
checked)
}
}
DankToggle {
width: parent.width
text: "Workspace Padding"
description: "Always show a minimum of 3 workspaces, even if fewer are available"
checked: SettingsData.showWorkspacePadding
onToggled: checked => {
return SettingsData.setShowWorkspacePadding(
checked)
}
}
}
}
StyledRect {
width: parent.width
height: workspaceIconsSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
visible: SettingsData.hasNamedWorkspaces()
Column {
id: workspaceIconsSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "label"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Named Workspace Icons"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
width: parent.width
text: "Configure icons for named workspaces. Icons take priority over numbers when both are enabled."
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
wrapMode: Text.WordWrap
}
Repeater {
model: SettingsData.getNamedWorkspaces()
Rectangle {
width: parent.width
height: workspaceIconRow.implicitHeight + Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r,
Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.5)
border.color: Qt.rgba(Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.3)
border.width: 1
Row {
id: workspaceIconRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
StyledText {
text: "\"" + modelData + "\""
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
width: 150
elide: Text.ElideRight
}
DankIconPicker {
id: iconPicker
anchors.verticalCenter: parent.verticalCenter
Component.onCompleted: {
var iconData = SettingsData.getWorkspaceNameIcon(
modelData)
if (iconData) {
setIcon(iconData.value,
iconData.type)
}
}
onIconSelected: (iconName, iconType) => {
SettingsData.setWorkspaceNameIcon(
modelData, {
"type": iconType,
"value": iconName
})
setIcon(iconName,
iconType)
}
Connections {
target: SettingsData
function onWorkspaceIconsUpdated() {
var iconData = SettingsData.getWorkspaceNameIcon(
modelData)
if (iconData) {
iconPicker.setIcon(
iconData.value,
iconData.type)
} else {
iconPicker.setIcon("", "icon")
}
}
}
}
Rectangle {
width: 28
height: 28
radius: Theme.cornerRadius
color: clearMouseArea.containsMouse ? Theme.errorHover : Theme.surfaceContainer
border.color: clearMouseArea.containsMouse ? Theme.error : Theme.outline
border.width: 1
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "close"
size: 16
color: clearMouseArea.containsMouse ? Theme.error : Theme.outline
anchors.centerIn: parent
}
MouseArea {
id: clearMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
SettingsData.removeWorkspaceNameIcon(
modelData)
}
}
}
Item {
width: parent.width - 150 - 240 - 28 - Theme.spacingM * 4
height: 1
}
}
}
}
}
}
}
}
DankWidgetSelectionPopup {
id: widgetSelectionPopup
anchors.centerIn: parent
onWidgetSelected: (widgetId, targetSection) => {
widgetsTab.addWidgetToSection(widgetId,
targetSection)
}
}
}

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