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
226 changed files with 32 additions and 63309 deletions
-55
View File
@@ -1,55 +0,0 @@
---
name: Bug Report
about: Crashes or unexpected behaviors
title: ""
labels: "bug"
assignees: ""
---
<!-- If your issue is related to ICONS
- Purple and black checkerboards are QT's way of signalling an icon doesn't exist
- FIX: Configure a QT6 or Icon Pack in DMS Settings that has the icon you want
- Follow the [THEMING](https://github.com/AvengeMedia/DankMaterialShell/tree/master?tab=readme-ov-file#theming) section to ensure your QT environment variable is configured correctl for themes.
- Once done, configure an icon theme - either however you normally do with gtk3 or qt6ct, or through the built-in settings modal. -->
<!-- If your issue is related to APP LAUNCHER/DOCK/Running Apps being stale
Quickshell does not ever update its DesktopEntires.
There is an open PR for it, that has been stuck unmerged over there to fix it.
We unfortunately are at the mercy of quickshell to merge it.
Until then, newly installed and removed apps will not react until the
shell is restarted.
-->
## Compositor
- [ ] niri
- [ ] Hyprland
- [ ] Other (specify)
## 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 -->
-31
View File
@@ -1,31 +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 -->
## Compositor
Is this feature specific to one compositor?
- [ ] All compositors
- [ ] niri
- [ ] Hyprland
## 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 -->
-30
View File
@@ -1,30 +0,0 @@
---
name: Request Assistance or Support
about: Help with installation, usage, or general questions.
title: ""
labels: "support"
assignees: ""
---
## Compositor
- [ ] niri
- [ ] Hyprland
- [ ] other
## 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/[email protected]
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
│ ├── DisplayService.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, DisplayService, 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: DisplayService.brightnessAvailable
enabled: DisplayService.brightnessAvailable
value: DisplayService.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)
}
}
-16
View File
@@ -1,16 +0,0 @@
pragma Singleton
import Quickshell
import QtQuick
Singleton {
id: modalManager
signal closeAllModalsExcept(var excludedModal)
function openModal(modal) {
if (!modal.allowStacking) {
closeAllModalsExcept(modal)
}
}
}
-55
View File
@@ -1,55 +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)])
}
// ! Spotify and maybe some other apps report the wrong app id in toplevels, hardcode special case
function moddedAppId(appId: string): string {
if (appId === "Spotify")
return "spotify-launcher"
return appId
}
}
-9
View File
@@ -1,9 +0,0 @@
import QtQuick
import Quickshell
QtObject {
required property Singleton service
Component.onCompleted: service.refCount++
Component.onDestruction: service.refCount--
}
-390
View File
@@ -1,390 +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 bool nightModeAutoEnabled: false
property string nightModeAutoMode: "time"
property int nightModeStartHour: 18
property int nightModeStartMinute: 0
property int nightModeEndHour: 6
property int nightModeEndMinute: 0
property real latitude: 0.0
property real longitude: 0.0
property string nightModeLocationProvider: ""
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: ""
property string notepadContent: ""
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
nightModeAutoEnabled = settings.nightModeAutoEnabled
!== undefined ? settings.nightModeAutoEnabled : false
nightModeAutoMode = settings.nightModeAutoMode
!== undefined ? settings.nightModeAutoMode : "time"
// Handle legacy time format
if (settings.nightModeStartTime !== undefined) {
const parts = settings.nightModeStartTime.split(":")
nightModeStartHour = parseInt(parts[0]) || 18
nightModeStartMinute = parseInt(parts[1]) || 0
} else {
nightModeStartHour = settings.nightModeStartHour !== undefined ? settings.nightModeStartHour : 18
nightModeStartMinute = settings.nightModeStartMinute !== undefined ? settings.nightModeStartMinute : 0
}
if (settings.nightModeEndTime !== undefined) {
const parts = settings.nightModeEndTime.split(":")
nightModeEndHour = parseInt(parts[0]) || 6
nightModeEndMinute = parseInt(parts[1]) || 0
} else {
nightModeEndHour = settings.nightModeEndHour !== undefined ? settings.nightModeEndHour : 6
nightModeEndMinute = settings.nightModeEndMinute !== undefined ? settings.nightModeEndMinute : 0
}
latitude = settings.latitude !== undefined ? settings.latitude : 0.0
longitude = settings.longitude !== undefined ? settings.longitude : 0.0
nightModeLocationProvider = settings.nightModeLocationProvider !== undefined ? settings.nightModeLocationProvider : ""
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 : ""
notepadContent = settings.notepadContent !== undefined ? settings.notepadContent : ""
}
} catch (e) {
}
}
function saveSettings() {
settingsFile.setText(JSON.stringify({
"isLightMode": isLightMode,
"wallpaperPath": wallpaperPath,
"wallpaperLastPath": wallpaperLastPath,
"profileLastPath": profileLastPath,
"doNotDisturb": doNotDisturb,
"nightModeEnabled": nightModeEnabled,
"nightModeTemperature": nightModeTemperature,
"nightModeAutoEnabled": nightModeAutoEnabled,
"nightModeAutoMode": nightModeAutoMode,
"nightModeStartHour": nightModeStartHour,
"nightModeStartMinute": nightModeStartMinute,
"nightModeEndHour": nightModeEndHour,
"nightModeEndMinute": nightModeEndMinute,
"latitude": latitude,
"longitude": longitude,
"nightModeLocationProvider": nightModeLocationProvider,
"pinnedApps": pinnedApps,
"selectedGpuIndex": selectedGpuIndex,
"nvidiaGpuTempEnabled": nvidiaGpuTempEnabled,
"nonNvidiaGpuTempEnabled": nonNvidiaGpuTempEnabled,
"enabledGpuPciIds": enabledGpuPciIds,
"wallpaperCyclingEnabled": wallpaperCyclingEnabled,
"wallpaperCyclingMode": wallpaperCyclingMode,
"wallpaperCyclingInterval": wallpaperCyclingInterval,
"wallpaperCyclingTime": wallpaperCyclingTime,
"lastBrightnessDevice": lastBrightnessDevice,
"notepadContent": notepadContent
}, 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 setNightModeAutoEnabled(enabled) {
console.log("SessionData: Setting nightModeAutoEnabled to", enabled)
nightModeAutoEnabled = enabled
saveSettings()
}
function setNightModeAutoMode(mode) {
nightModeAutoMode = mode
saveSettings()
}
function setNightModeStartHour(hour) {
nightModeStartHour = hour
saveSettings()
}
function setNightModeStartMinute(minute) {
nightModeStartMinute = minute
saveSettings()
}
function setNightModeEndHour(hour) {
nightModeEndHour = hour
saveSettings()
}
function setNightModeEndMinute(minute) {
nightModeEndMinute = minute
saveSettings()
}
function setLatitude(lat) {
console.log("SessionData: Setting latitude to", lat)
latitude = lat
saveSettings()
}
function setLongitude(lng) {
console.log("SessionData: Setting longitude to", lng)
longitude = lng
saveSettings()
}
function setNightModeLocationProvider(provider) {
nightModeLocationProvider = provider
saveSettings()
}
function setWallpaperPath(path) {
wallpaperPath = path
saveSettings()
}
function setWallpaper(imagePath) {
console.log("SessionData.setWallpaper called with:", imagePath)
wallpaperPath = imagePath
saveSettings()
if (typeof Theme !== "undefined") {
console.log("Theme is available, current theme:", Theme.currentTheme)
// Always extract colors for shell UI if dynamic theming is enabled
if (typeof SettingsData !== "undefined" && SettingsData.wallpaperDynamicTheming) {
console.log("Dynamic theming enabled, extracting colors")
Theme.extractColors()
}
// Always generate system themes (matugen templates) when wallpaper changes
console.log("Calling generateSystemThemesFromCurrentTheme")
Theme.generateSystemThemesFromCurrentTheme()
} else {
console.log("Theme is undefined!")
}
}
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()
}
}
}
}
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)
}
-738
View File
@@ -1,738 +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 var workerRunning: false
property var matugenColors: ({})
property bool extractionRequested: false
property int colorUpdateTrigger: 0
property var customThemeData: null
readonly property string stateDir: {
const cacheHome = StandardPaths.writableLocation(StandardPaths.CacheLocation).toString()
const path = cacheHome.startsWith("file://") ? cacheHome.substring(7) : cacheHome
return path + "/dankshell"
}
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 shorterDuration: 100
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 setLightMode(light, savePrefs = true) {
isLightMode = light
if (savePrefs && typeof SessionData !== "undefined")
SessionData.setLightMode(isLightMode)
PortalService.setLightMode(isLightMode)
generateSystemThemesFromCurrentTheme()
}
function toggleLightMode(savePrefs = true) {
setLightMode(!isLightMode, savePrefs)
}
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 setDesiredTheme(kind, value, isLight, iconTheme) {
if (!matugenAvailable) {
console.warn("matugen not available - cannot set system theme")
return
}
const desired = {
"kind": kind,
"value": value,
"mode": isLight ? "light" : "dark",
"iconTheme": iconTheme || "System Default"
}
const json = JSON.stringify(desired)
const desiredPath = stateDir + "/matugen.desired.json"
Quickshell.execDetached([
"sh", "-c",
`mkdir -p '${stateDir}' && cat > '${desiredPath}' << 'EOF'\n${json}\nEOF`
])
workerRunning = true
systemThemeGenerator.command = [shellDir + "/scripts/matugen-worker.sh", stateDir, shellDir, "--run"]
systemThemeGenerator.running = true
}
function generateSystemThemesFromCurrentTheme() {
if (!matugenAvailable)
return
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
if (currentTheme === dynamic) {
if (!wallpaperPath) {
return
}
setDesiredTheme("image", wallpaperPath, isLight, iconTheme)
} 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) {
console.warn("No primary color available for theme:", currentTheme)
return
}
setDesiredTheme("hex", primaryColor, isLight, iconTheme)
}
}
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
}
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
if (currentTheme === dynamic) {
if (wallpaperPath) {
// Clear cache on startup to force regeneration
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
setDesiredTheme("image", wallpaperPath, isLight, iconTheme)
} else {
}
} else {
let primaryColor
if (currentTheme === "custom") {
if (customThemeData && customThemeData.primary) {
primaryColor = customThemeData.primary
}
} else {
primaryColor = currentThemeData.primary
}
if (primaryColor) {
// Clear cache on startup to force regeneration
Quickshell.execDetached(["rm", "-f", stateDir + "/matugen.key"])
setDesiredTheme("hex", primaryColor, isLight, iconTheme)
} else {
}
}
}
}
Process {
id: fileChecker
command: ["test", "-r", wallpaperPath]
onExited: code => {
if (code === 0) {
matugenProcess.running = true
}
}
}
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++
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: ensureStateDir
}
Process {
id: systemThemeGenerator
running: false
onExited: exitCode => {
workerRunning = false
if (exitCode === 2) {
// Exit code 2 means wallpaper/color not found - this is expected on first run
console.log("Theme worker: wallpaper/color not found, skipping theme generation")
} else if (exitCode !== 0) {
if (typeof ToastService !== "undefined") {
ToastService.showError("Theme worker failed (" + exitCode + ")")
}
console.warn("Theme worker failed with exit code:", exitCode)
}
}
}
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)
}
}
}
}
Component.onCompleted: {
matugenCheck.running = true
if (typeof SessionData !== "undefined")
SessionData.isLightModeChanged.connect(root.onLightModeChanged)
}
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)
}
}
}
IpcHandler {
target: "theme"
function toggle(): string {
root.toggleLightMode()
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"
}
}
}
-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>.
-920
View File
@@ -1,920 +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 var clipboardEntries: []
property string searchText: ""
property int selectedIndex: 0
property bool keyboardNavigationActive: false
property bool showKeyboardHints: false
property Component clipboardContent
property int activeImageLoads: 0
readonly property int maxConcurrentLoads: 3
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 = ""
clipboardHistoryModal.activeImageLoads = 0
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 = ""
clipboardHistoryModal.activeImageLoads = 0
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
}
}
}
ConfirmModal {
id: clearConfirmDialog
confirmButtonText: "Clear All"
confirmButtonColor: Theme.primary
onVisibleChanged: {
if (visible) {
clipboardHistoryModal.shouldHaveFocus = false
} else if (clipboardHistoryModal.shouldBeVisible) {
clipboardHistoryModal.shouldHaveFocus = true
clipboardHistoryModal.modalFocusScope.forceActiveFocus()
if (clipboardHistoryModal.contentLoader.item && clipboardHistoryModal.contentLoader.item.searchField) {
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus()
}
}
}
}
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.surfaceText
hoverColor: Theme.surfaceHover
onClicked: {
clearConfirmDialog.show(
"Clear All History?",
"This will permanently delete all clipboard history.",
function() {
clearAll()
hide()
},
function() {} // No action on cancel
)
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.surfaceHover
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
Image {
id: thumbnailImageSource
property string entryId: model.entry.split(
'\t')[0]
property bool isVisible: false
property string cachedImageData: ""
property bool loadQueued: false
anchors.fill: parent
source: ""
fillMode: Image.PreserveAspectCrop
smooth: true
cache: false // Disable Qt's cache to control it ourselves
visible: false
asynchronous: true
sourceSize.width: 128
sourceSize.height: 128
onCachedImageDataChanged: {
if (cachedImageData) {
source = ""
source = `data:image/png;base64,${cachedImageData}`
}
}
function tryLoadImage() {
if (!loadQueued && entryType === "image" && !cachedImageData) {
loadQueued = true
if (clipboardHistoryModal.activeImageLoads < clipboardHistoryModal.maxConcurrentLoads) {
clipboardHistoryModal.activeImageLoads++
imageLoader.running = true
} else {
// Retry after delay
retryTimer.restart()
}
}
}
Timer {
id: retryTimer
interval: 50
onTriggered: {
if (thumbnailImageSource.loadQueued && !imageLoader.running) {
if (clipboardHistoryModal.activeImageLoads < clipboardHistoryModal.maxConcurrentLoads) {
clipboardHistoryModal.activeImageLoads++
imageLoader.running = true
} else {
retryTimer.restart()
}
}
}
}
Component.onCompleted: {
if (entryType !== "image") return
// Check if item is visible on screen initially
let itemY = index * (72 + clipboardListView.spacing)
let viewTop = clipboardListView.contentY
let viewBottom = viewTop + clipboardListView.height
isVisible = (itemY + 72 >= viewTop && itemY <= viewBottom)
if (isVisible) {
tryLoadImage()
}
}
Connections {
target: clipboardListView
function onContentYChanged() {
if (entryType !== "image") return
let itemY = index * (72 + clipboardListView.spacing)
let viewTop = clipboardListView.contentY - 100 // Preload slightly before visible
let viewBottom = viewTop + clipboardListView.height + 200
let nowVisible = (itemY + 72 >= viewTop && itemY <= viewBottom)
if (nowVisible && !thumbnailImageSource.isVisible) {
thumbnailImageSource.isVisible = true
thumbnailImageSource.tryLoadImage()
}
}
}
Process {
id: imageLoader
running: false
command: ["sh", "-c", `cliphist decode ${thumbnailImageSource.entryId} | base64 -w 0`]
stdout: StdioCollector {
onStreamFinished: {
let imageData = text.trim()
if (imageData && imageData.length > 0) {
thumbnailImageSource.cachedImageData = imageData
}
}
}
onExited: exitCode => {
thumbnailImageSource.loadQueued = false
if (clipboardHistoryModal.activeImageLoads > 0) {
clipboardHistoryModal.activeImageLoads--
}
if (exitCode !== 0) {
console.warn("Failed to load clipboard image:", thumbnailImageSource.entryId)
}
}
}
}
MultiEffect {
anchors.fill: parent
anchors.margins: 2
source: thumbnailImageSource
maskEnabled: true
maskSource: clipboardCircularMask
visible: entryType === "image"
&& thumbnailImageSource.status === Image.Ready
&& thumbnailImageSource.source != ""
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
&& thumbnailImageSource.source != "")
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.surfaceText
hoverColor: Theme.surfaceHover
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
}
}
}
}
}
}
-229
View File
@@ -1,229 +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 confirmTitle: ""
property string confirmMessage: ""
property string confirmButtonText: "Confirm"
property string cancelButtonText: "Cancel"
property color confirmButtonColor: Theme.primary
property var onConfirm: function() {}
property var onCancel: function() {}
property int selectedButton: -1 // -1 = none, 0 = Cancel, 1 = Confirm
property bool keyboardNavigation: false
function show(title, message, onConfirmCallback, onCancelCallback) {
confirmTitle = title || ""
confirmMessage = message || ""
confirmButtonText = "Confirm"
cancelButtonText = "Cancel"
confirmButtonColor = Theme.primary
onConfirm = onConfirmCallback || function() {}
onCancel = onCancelCallback || function() {}
selectedButton = -1
keyboardNavigation = false
open()
}
function showWithOptions(options) {
confirmTitle = options.title || ""
confirmMessage = options.message || ""
confirmButtonText = options.confirmText || "Confirm"
cancelButtonText = options.cancelText || "Cancel"
confirmButtonColor = options.confirmColor || Theme.primary
onConfirm = options.onConfirm || function() {}
onCancel = options.onCancel || function() {}
selectedButton = -1
keyboardNavigation = false
open()
}
function selectButton() {
if (selectedButton === 0) {
close()
if (onCancel) onCancel()
} else {
close()
if (onConfirm) onConfirm()
}
}
shouldBeVisible: false
allowStacking: true
width: 350
height: 160
enableShadow: true
shouldHaveFocus: true
onBackgroundClicked: {
close()
if (onCancel) onCancel()
}
onOpened: {
modalFocusScope.forceActiveFocus()
modalFocusScope.focus = true
shouldHaveFocus = true
}
modalFocusScope.Keys.onPressed: function(event) {
switch (event.key) {
case Qt.Key_Escape:
close()
if (onCancel) onCancel()
event.accepted = true
break
case Qt.Key_Left:
case Qt.Key_Up:
keyboardNavigation = true
selectedButton = 0
event.accepted = true
break
case Qt.Key_Right:
case Qt.Key_Down:
keyboardNavigation = true
selectedButton = 1
event.accepted = true
break
case Qt.Key_Tab:
keyboardNavigation = true
selectedButton = selectedButton === -1 ? 0 : (selectedButton + 1) % 2
event.accepted = true
break
case Qt.Key_Return:
case Qt.Key_Enter:
if (selectedButton !== -1) {
selectButton()
} else {
selectedButton = 1
selectButton()
}
event.accepted = true
break
}
}
content: Component {
Item {
anchors.fill: parent
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingM
StyledText {
text: confirmTitle
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: confirmMessage
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: {
if (keyboardNavigation && selectedButton === 0)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
else if (cancelButton.containsMouse)
return Theme.surfacePressed
else
return Theme.surfaceVariantAlpha
}
border.color: (keyboardNavigation && selectedButton === 0) ? Theme.primary : "transparent"
border.width: (keyboardNavigation && selectedButton === 0) ? 1 : 0
StyledText {
text: cancelButtonText
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: {
selectedButton = 0
selectButton()
}
}
}
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: {
let baseColor = confirmButtonColor
if (keyboardNavigation && selectedButton === 1)
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1)
else if (confirmButton.containsMouse)
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9)
else
return baseColor
}
border.color: (keyboardNavigation && selectedButton === 1) ? "white" : "transparent"
border.width: (keyboardNavigation && selectedButton === 1) ? 1 : 0
StyledText {
text: confirmButtonText
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: {
selectedButton = 1
selectButton()
}
}
}
}
}
}
}
}
-241
View File
@@ -1,241 +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.shorterDuration
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 => {
if (root.closeOnEscapeKey
&& shouldHaveFocus) {
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()
})
}
}
}
}
}
-276
View File
@@ -1,276 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtCore
import Qt.labs.folderlistmodel
import Quickshell.Io
import qs.Common
import qs.Widgets
Rectangle {
id: root
property bool showFileInfo: false
property int selectedIndex: -1
property var sourceFolderModel: null
property string currentPath: ""
height: 200
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.95)
border.color: Theme.secondary
border.width: 2
opacity: showFileInfo ? 1 : 0
z: 100
onShowFileInfoChanged: {
if (showFileInfo && currentFileName && currentPath) {
var fullPath = currentPath + "/" + currentFileName
fileStatProcess.selectedFilePath = fullPath
fileStatProcess.running = true
}
}
Process {
id: fileStatProcess
command: ["stat", "-c", "%y|%A|%s|%n", selectedFilePath]
property string selectedFilePath: ""
property var fileStats: null
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
var parts = text.trim().split('|')
if (parts.length >= 4) {
fileStatProcess.fileStats = {
modifiedTime: parts[0],
permissions: parts[1],
size: parseInt(parts[2]) || 0,
fullPath: parts[3]
}
}
}
}
}
onExited: function(exitCode) {}
}
readonly property var currentFileData: {
if (!sourceFolderModel || selectedIndex < 0 || selectedIndex >= sourceFolderModel.count) {
return {
exists: false,
name: "No selection",
type: "",
size: "",
modified: "",
permissions: "",
extension: "",
position: selectedIndex >= 0 ? (selectedIndex + 1) + " of " + (sourceFolderModel ? sourceFolderModel.count : 0) : "N/A"
}
}
var path = currentPath
if (path && selectedIndex >= 0) {
return {
exists: true,
name: "Loading...",
type: "file",
size: "Calculating...",
modified: "Loading...",
permissions: "Loading...",
extension: "",
position: (selectedIndex + 1) + " of " + sourceFolderModel.count
}
}
return {
exists: false,
name: "No selection",
type: "",
size: "",
modified: "",
permissions: "",
extension: "",
position: "N/A"
}
}
property string currentFileName: ""
property bool currentFileIsDir: false
property string currentFileExtension: ""
onCurrentFileNameChanged: {
if (showFileInfo && currentFileName && currentPath) {
var fullPath = currentPath + "/" + currentFileName
if (fullPath !== fileStatProcess.selectedFilePath) {
fileStatProcess.selectedFilePath = fullPath
fileStatProcess.running = true
}
}
}
function updateFileInfo(filePath, fileName, isDirectory) {
if (filePath && filePath !== fileStatProcess.selectedFilePath) {
fileStatProcess.selectedFilePath = filePath
currentFileName = fileName || ""
currentFileIsDir = isDirectory || false
var ext = ""
if (!isDirectory && fileName) {
var lastDot = fileName.lastIndexOf('.')
if (lastDot > 0) {
ext = fileName.substring(lastDot + 1).toLowerCase()
}
}
currentFileExtension = ext
if (showFileInfo) {
fileStatProcess.running = true
}
}
}
readonly property var currentFileDisplayData: {
if (selectedIndex < 0 || !sourceFolderModel) {
return {
exists: false,
name: "No selection",
type: "",
size: "",
modified: "",
permissions: "",
extension: "",
position: "N/A"
}
}
var hasValidFile = currentFileName !== ""
return {
exists: hasValidFile,
name: hasValidFile ? currentFileName : "Loading...",
type: currentFileIsDir ? "Directory" : "File",
size: fileStatProcess.fileStats ? formatFileSize(fileStatProcess.fileStats.size) : "Calculating...",
modified: fileStatProcess.fileStats ? formatDateTime(fileStatProcess.fileStats.modifiedTime) : "Loading...",
permissions: fileStatProcess.fileStats ? fileStatProcess.fileStats.permissions : "Loading...",
extension: currentFileExtension,
position: sourceFolderModel ? ((selectedIndex + 1) + " of " + sourceFolderModel.count) : "N/A"
}
}
Column {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "info"
size: Theme.iconSize
color: Theme.secondary
}
StyledText {
text: "File Information"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: currentFileDisplayData.name
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideMiddle
wrapMode: Text.NoWrap
font.weight: Font.Medium
}
StyledText {
text: currentFileDisplayData.type + (currentFileDisplayData.extension ? " (." + currentFileDisplayData.extension + ")" : "")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
width: parent.width
}
StyledText {
text: currentFileDisplayData.size
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
width: parent.width
visible: currentFileDisplayData.exists && !currentFileIsDir
}
StyledText {
text: currentFileDisplayData.modified
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
width: parent.width
elide: Text.ElideRight
visible: currentFileDisplayData.exists
}
StyledText {
text: currentFileDisplayData.permissions
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
visible: currentFileDisplayData.exists
}
StyledText {
text: currentFileDisplayData.position
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
width: parent.width
}
}
}
StyledText {
text: "F1/I: Toggle • F10: Help"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingM
horizontalAlignment: Text.AlignHCenter
}
function formatFileSize(bytes) {
if (bytes === 0 || !bytes) return "0 B"
var k = 1024
var sizes = ['B', 'KB', 'MB', 'GB', 'TB']
var i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
function formatDateTime(dateTimeString) {
if (!dateTimeString) return "Unknown"
var parts = dateTimeString.split(' ')
if (parts.length >= 2) {
return parts[0] + " " + parts[1].split('.')[0]
}
return dateTimeString
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
-51
View File
@@ -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: "Tab/Shift+Tab: Nav • ←→↑↓: Grid Nav • Enter/Space: Select"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: "Alt+←/Backspace: Back • F1/I: File Info • 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
}
}
}
-727
View File
@@ -1,727 +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
property int selectedIndex: -1
property bool keyboardNavigationActive: false
property bool backButtonFocused: false
property bool saveMode: false // Enable save functionality
property string defaultFileName: "" // Default filename for save mode
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()
onOpened: {
modalFocusScope.forceActiveFocus()
}
modalFocusScope.Keys.onPressed: function(event) {
keyboardController.handleKey(event)
}
onVisibleChanged: {
if (visible) {
var startPath = getLastPath()
currentPath = startPath
selectedIndex = -1
keyboardNavigationActive = false
backButtonFocused = false
}
}
onCurrentPathChanged: {
selectedFilePath = ""
selectedFileName = ""
selectedFileIsDir = false
}
onSelectedIndexChanged: {
// Update selected file data when index changes
if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) {
// We need to get the file data from the model for this index
// This is a bit tricky with FolderListModel, so we'll use a different approach
selectedFilePath = ""
selectedFileName = ""
selectedFileIsDir = false
}
}
// Function to update file data from delegates
function setSelectedFileData(path, name, isDir) {
selectedFilePath = path
selectedFileName = name
selectedFileIsDir = isDir
}
QtObject {
id: keyboardController
property int totalItems: folderModel.count
property int gridColumns: 5 // Fixed number of columns for the grid (matches actual display)
function handleKey(event) {
if (event.key === Qt.Key_Escape) {
close()
event.accepted = true
return
}
// F10 toggles keyboard hints
if (event.key === Qt.Key_F10) {
showKeyboardHints = !showKeyboardHints
event.accepted = true
return
}
// F1 or I key for file information
if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) {
showFileInfo = !showFileInfo
event.accepted = true
return
}
// Alt+Left or Backspace to go back
if ((event.modifiers & Qt.AltModifier && event.key === Qt.Key_Left) || event.key === Qt.Key_Backspace) {
if (currentPath !== homeDir) {
navigateUp()
event.accepted = true
}
return
}
if (!keyboardNavigationActive) {
if (event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key === Qt.Key_Right) {
keyboardNavigationActive = true
if (currentPath !== homeDir) {
backButtonFocused = true
selectedIndex = -1
} else {
backButtonFocused = false
selectedIndex = 0
}
event.accepted = true
}
return
}
switch (event.key) {
case Qt.Key_Tab:
if (backButtonFocused) {
backButtonFocused = false
selectedIndex = 0
} else if (selectedIndex < totalItems - 1) {
selectedIndex++
} else if (currentPath !== homeDir) {
backButtonFocused = true
selectedIndex = -1
} else {
selectedIndex = 0
}
event.accepted = true
break
case Qt.Key_Backtab:
if (backButtonFocused) {
backButtonFocused = false
selectedIndex = totalItems - 1
} else if (selectedIndex > 0) {
selectedIndex--
} else if (currentPath !== homeDir) {
backButtonFocused = true
selectedIndex = -1
} else {
selectedIndex = totalItems - 1
}
event.accepted = true
break
case Qt.Key_Left:
if (backButtonFocused) {
return
}
if (selectedIndex > 0) {
selectedIndex--
// Update file info for navigation
updateFileInfoForIndex(selectedIndex)
} else if (currentPath !== homeDir) {
backButtonFocused = true
selectedIndex = -1
}
event.accepted = true
break
case Qt.Key_Right:
if (backButtonFocused) {
backButtonFocused = false
selectedIndex = 0
updateFileInfoForIndex(selectedIndex)
} else if (selectedIndex < totalItems - 1) {
selectedIndex++
updateFileInfoForIndex(selectedIndex)
}
event.accepted = true
break
case Qt.Key_Up:
if (backButtonFocused) {
backButtonFocused = false
// Go to first row, appropriate column
var col = selectedIndex % gridColumns
selectedIndex = Math.min(col, totalItems - 1)
updateFileInfoForIndex(selectedIndex)
} else if (selectedIndex >= gridColumns) {
// Move up one row
selectedIndex -= gridColumns
updateFileInfoForIndex(selectedIndex)
} else if (currentPath !== homeDir) {
// At top row, go to back button
backButtonFocused = true
selectedIndex = -1
}
event.accepted = true
break
case Qt.Key_Down:
if (backButtonFocused) {
backButtonFocused = false
selectedIndex = 0
updateFileInfoForIndex(selectedIndex)
} else {
// Move down one row if possible
var newIndex = selectedIndex + gridColumns
if (newIndex < totalItems) {
selectedIndex = newIndex
updateFileInfoForIndex(selectedIndex)
} else {
// If can't go down a full row, go to last item in the column if exists
var lastRowStart = Math.floor((totalItems - 1) / gridColumns) * gridColumns
var col = selectedIndex % gridColumns
var targetIndex = lastRowStart + col
if (targetIndex < totalItems && targetIndex > selectedIndex) {
selectedIndex = targetIndex
updateFileInfoForIndex(selectedIndex)
}
}
}
event.accepted = true
break
case Qt.Key_Return:
case Qt.Key_Enter:
case Qt.Key_Space:
if (backButtonFocused) {
navigateUp()
} else if (selectedIndex >= 0 && selectedIndex < totalItems) {
// Trigger selection by setting the grid's current index and using signal
fileBrowserModal.keyboardFileSelection(selectedIndex)
}
event.accepted = true
break
}
// Scroll handling is done in the grid's onCurrentIndexChanged
}
}
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
selectedIndex = -1
backButtonFocused = false
}
function keyboardFileSelection(index) {
if (index >= 0) {
keyboardSelectionTimer.targetIndex = index
keyboardSelectionTimer.start()
}
}
function updateSelectedFileInfo(index) {
// This will be called when we need to update file info for the selected index
// The delegate will handle the actual file info updates
}
function updateFileInfoForIndex(index) {
// We can't directly access FolderListModel data by index from here
// Instead, we'll rely on the delegate's Component.onCompleted and mouse clicks
// to call setSelectedFileData() with the proper file information
// For keyboard navigation, we need a different approach
// The selectedIndex change will trigger delegate updates
}
Timer {
id: keyboardSelectionTimer
interval: 1
property int targetIndex: -1
onTriggered: {
// Access the currently selected item through model role names
// This will work because QML models expose role data
executeKeyboardSelection(targetIndex)
}
}
function executeKeyboardSelection(index) {
// This is a simplified version that just needs to work
// We'll handle this in the mouse area of each delegate
// For now, signal that keyboard selection was requested
keyboardSelectionIndex = index
keyboardSelectionRequested = true
}
property int keyboardSelectionIndex: -1
property bool keyboardSelectionRequested: false
property bool showKeyboardHints: false
property bool showFileInfo: false
property string selectedFilePath: ""
property string selectedFileName: ""
property bool selectedFileIsDir: false
content: Component {
Item {
anchors.fill: parent
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
}
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankActionButton {
circular: false
iconName: "help"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.surfacePressed
onClicked: fileBrowserModal.showKeyboardHints = !fileBrowserModal.showKeyboardHints
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: fileBrowserModal.close()
}
}
}
Row {
width: parent.width
spacing: Theme.spacingS
StyledRect {
width: 32
height: 32
radius: Theme.cornerRadius
color: (backButtonMouseArea.containsMouse || (backButtonFocused && keyboardNavigationActive))
&& 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: backButtonMouseArea
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
currentIndex: selectedIndex
onCurrentIndexChanged: {
if (keyboardNavigationActive && currentIndex >= 0) {
positionViewAtIndex(currentIndex, GridView.Contain)
}
}
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
required property int index
width: 140
height: 120
radius: Theme.cornerRadius
color: {
if (keyboardNavigationActive && delegateRoot.index === selectedIndex) {
return Theme.surfacePressed
}
return mouseArea.containsMouse ? Theme.surfaceVariant : "transparent"
}
border.color: keyboardNavigationActive && delegateRoot.index === selectedIndex ? Theme.primary : Theme.outline
border.width: (mouseArea.containsMouse || (keyboardNavigationActive && delegateRoot.index === selectedIndex)) ? 1 : 0
// Update file info when this item gets selected via keyboard or initially
Component.onCompleted: {
if (keyboardNavigationActive && delegateRoot.index === selectedIndex) {
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
}
}
// Watch for selectedIndex changes to update file info during keyboard navigation
Connections {
target: fileBrowserModal
function onSelectedIndexChanged() {
if (keyboardNavigationActive && selectedIndex === delegateRoot.index) {
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
}
}
}
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: {
// Update selected file info and index first
selectedIndex = delegateRoot.index
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
if (delegateRoot.fileIsDir) {
navigateTo(delegateRoot.filePath)
} else {
fileSelected(delegateRoot.filePath)
fileBrowserModal.close() // Close modal after file selection
}
}
}
// Handle keyboard selection
Connections {
target: fileBrowserModal
function onKeyboardSelectionRequestedChanged() {
if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === delegateRoot.index) {
// Reset the flag first
fileBrowserModal.keyboardSelectionRequested = false
// Update selected file info and index first
selectedIndex = delegateRoot.index
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
// Trigger the same action as mouse click
if (delegateRoot.fileIsDir) {
navigateTo(delegateRoot.filePath)
} else {
fileSelected(delegateRoot.filePath)
fileBrowserModal.close()
}
}
}
}
}
}
}
// Save functionality - positioned at bottom in save mode
Row {
id: saveRow
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
height: saveMode ? 40 : 0
visible: saveMode
spacing: Theme.spacingM
DankTextField {
id: fileNameInput
width: parent.width - saveButton.width - Theme.spacingM
height: 36
text: defaultFileName
placeholderText: "Enter filename..."
ignoreLeftRightKeys: false // Allow arrow key navigation
focus: saveMode // Auto-focus when in save mode
Component.onCompleted: {
if (saveMode) {
Qt.callLater(() => {
forceActiveFocus();
});
}
}
onAccepted: {
if (text.trim() !== "") {
var fullPath = currentPath + "/" + text.trim();
fileSelected(fullPath);
fileBrowserModal.close();
}
}
}
StyledRect {
id: saveButton
width: 80
height: 36
color: fileNameInput.text.trim() !== "" ? Theme.primary : Theme.surfaceVariant
radius: Theme.cornerRadius
StyledText {
anchors.centerIn: parent
text: "Save"
color: fileNameInput.text.trim() !== "" ? Theme.primaryText : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeMedium
}
StateLayer {
stateColor: Theme.primary
cornerRadius: Theme.cornerRadius
enabled: fileNameInput.text.trim() !== ""
onClicked: {
if (fileNameInput.text.trim() !== "") {
var fullPath = currentPath + "/" + fileNameInput.text.trim();
fileSelected(fullPath);
fileBrowserModal.close();
}
}
}
}
}
FileBrowserKeyboardHints {
id: keyboardHints
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
showHints: fileBrowserModal.showKeyboardHints
}
FileBrowserFileInfo {
id: fileInfo
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Theme.spacingL
width: 300
showFileInfo: fileBrowserModal.showFileInfo
selectedIndex: fileBrowserModal.selectedIndex
sourceFolderModel: folderModel
currentPath: fileBrowserModal.currentPath
// Bind directly to the modal's selected file properties
currentFileName: fileBrowserModal.selectedFileName
currentFileIsDir: fileBrowserModal.selectedFileIsDir
currentFileExtension: {
if (fileBrowserModal.selectedFileIsDir || !fileBrowserModal.selectedFileName) return ""
var lastDot = fileBrowserModal.selectedFileName.lastIndexOf('.')
return lastDot > 0 ? fileBrowserModal.selectedFileName.substring(lastDot + 1).toLowerCase() : ""
}
}
}
}
}
-175
View File
@@ -1,175 +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()
}
}
}
DankFlickable {
id: flickableArea
width: parent.width
height: parent.height - 140
clip: true
contentWidth: width
contentHeight: detailsRect.height
Rectangle {
id: detailsRect
width: parent.width
height: detailsText.contentHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: Theme.outlineStrong
border.width: 1
TextArea {
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
readOnly: true
selectByMouse: true
background: null
padding: 0
}
}
}
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
}
}
}
}
}
}
}
}
-399
View File
@@ -1,399 +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 bool notepadModalVisible: false
property bool fileDialogOpen: false
property string currentFileName: "" // Track the currently loaded file
function show() {
notepadModalVisible = true;
shouldHaveFocus = Qt.binding(() => {
return notepadModalVisible && !fileDialogOpen;
});
open();
}
function hide() {
notepadModalVisible = false;
// Clear filename when closing (so it doesn't persist between sessions)
currentFileName = "";
close();
}
function toggle() {
if (notepadModalVisible)
hide();
else
show();
}
visible: notepadModalVisible
width: 700
height: 500
enableShadow: true
onShouldHaveFocusChanged: {
console.log("Notepad: shouldHaveFocus changed to", shouldHaveFocus, "modalVisible:", notepadModalVisible, "dialogOpen:", fileDialogOpen);
}
onBackgroundClicked: hide()
content: Component {
Item {
id: contentItem
anchors.fill: parent
property alias textArea: textArea
Connections {
target: root
function onNotepadModalVisibleChanged() {
if (root.notepadModalVisible) {
Qt.callLater(() => {
textArea.forceActiveFocus();
});
}
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
height: 40
Row {
width: parent.width - closeButton.width
spacing: Theme.spacingM
Column {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Notepad"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: currentFileName
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
visible: currentFileName !== ""
elide: Text.ElideMiddle
maximumLineCount: 1
width: 200
}
}
StyledText {
text: SessionData.notepadContent.length > 0 ? `${SessionData.notepadContent.length} characters` : "Empty"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
anchors.verticalCenter: parent.verticalCenter
}
}
DankActionButton {
id: closeButton
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: root.hide()
}
}
StyledRect {
width: parent.width
height: parent.height - 80
color: Theme.surface
border.color: Theme.outlineMedium
border.width: 1
radius: Theme.cornerRadius
ScrollView {
id: scrollView
anchors.fill: parent
anchors.margins: 1
clip: true
TextArea {
id: textArea
text: SessionData.notepadContent
placeholderText: "Start typing your notes here..."
font.family: SettingsData.monoFontFamily
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
selectByMouse: true
selectByKeyboard: true
wrapMode: TextArea.Wrap
focus: root.notepadModalVisible
activeFocusOnTab: true
onTextChanged: {
if (text !== SessionData.notepadContent) {
SessionData.notepadContent = text;
saveTimer.restart();
}
}
Keys.onEscapePressed: (event) => {
root.hide();
event.accepted = true;
}
Component.onCompleted: {
if (root.notepadModalVisible) {
Qt.callLater(() => {
forceActiveFocus();
});
}
}
background: Rectangle {
color: "transparent"
}
}
}
}
Row {
width: parent.width
height: 32
spacing: Theme.spacingL
Row {
spacing: Theme.spacingS
DankActionButton {
iconName: "save"
iconSize: Theme.iconSize - 2
iconColor: Theme.primary
hoverColor: Theme.primaryHover
onClicked: {
console.log("Notepad: Opening save dialog, releasing modal focus");
root.allowFocusOverride = true;
root.shouldHaveFocus = false;
fileDialogOpen = true;
saveBrowser.open();
}
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: "Save to file"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
Row {
spacing: Theme.spacingS
DankActionButton {
iconName: "folder_open"
iconSize: Theme.iconSize - 2
iconColor: Theme.secondary
hoverColor: Theme.secondaryHover
onClicked: {
console.log("Notepad: Opening load dialog, releasing modal focus");
root.allowFocusOverride = true;
root.shouldHaveFocus = false;
fileDialogOpen = true;
loadBrowser.open();
}
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: "Load file"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
Item {
width: 1
height: 1
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: saveTimer.running ? "Auto-saving..." : "Auto-saved"
font.pixelSize: Theme.fontSizeSmall
color: saveTimer.running ? Theme.primary : Theme.surfaceTextMedium
opacity: SessionData.notepadContent.length > 0 ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
Timer {
id: saveTimer
interval: 1000
repeat: false
onTriggered: SessionData.saveSettings()
}
FileBrowserModal {
id: saveBrowser
browserTitle: "Save Notepad File"
browserIcon: "save"
browserType: "notepad_save"
fileExtensions: ["*.txt", "*.*"]
allowStacking: true
saveMode: true
defaultFileName: "note.txt"
onFileSelected: (path) => {
fileDialogOpen = false;
selectedFilePath = path;
const content = textArea.text;
if (content.length > 0) {
writeFileProcess.command = ["sh", "-c", `echo '${content.replace(/'/g, "'\\''")}' > '${path}'`];
writeFileProcess.running = true;
}
close();
// Restore modal focus
root.allowFocusOverride = false;
root.shouldHaveFocus = Qt.binding(() => {
return root.notepadModalVisible && !fileDialogOpen;
});
// Restore focus to TextArea after dialog closes
Qt.callLater(() => {
textArea.forceActiveFocus();
});
}
onDialogClosed: {
fileDialogOpen = false;
// Restore modal focus
root.allowFocusOverride = false;
root.shouldHaveFocus = Qt.binding(() => {
return root.notepadModalVisible && !fileDialogOpen;
});
// Restore focus to TextArea after dialog closes
Qt.callLater(() => {
textArea.forceActiveFocus();
});
}
property string selectedFilePath: ""
}
FileBrowserModal {
id: loadBrowser
browserTitle: "Load Notepad File"
browserIcon: "folder_open"
browserType: "notepad_load"
fileExtensions: ["*.txt", "*.*"]
allowStacking: true
onFileSelected: (path) => {
fileDialogOpen = false;
// Clean the file path - remove file:// prefix if present
var cleanPath = path.toString().replace(/^file:\/\//, '');
// Extract filename from path
var fileName = cleanPath.split('/').pop();
currentFileName = fileName;
console.log("Notepad: Loading file from path:", cleanPath);
readFileProcess.command = ["cat", cleanPath];
readFileProcess.running = true;
close();
// Restore modal focus
root.allowFocusOverride = false;
root.shouldHaveFocus = Qt.binding(() => {
return root.notepadModalVisible && !fileDialogOpen;
});
// Restore focus to TextArea after dialog closes
Qt.callLater(() => {
textArea.forceActiveFocus();
});
}
onDialogClosed: {
fileDialogOpen = false;
// Restore modal focus
root.allowFocusOverride = false;
root.shouldHaveFocus = Qt.binding(() => {
return root.notepadModalVisible && !fileDialogOpen;
});
// Restore focus to TextArea after dialog closes
Qt.callLater(() => {
textArea.forceActiveFocus();
});
}
}
Process {
id: writeFileProcess
command: []
running: false
onExited: (exitCode) => {
if (exitCode === 0)
console.log("Notepad: File saved successfully");
else
console.warn("Notepad: Failed to save file, exit code:", exitCode);
}
}
Process {
id: readFileProcess
command: []
running: false
stdout: StdioCollector {
onStreamFinished: {
console.log("Notepad: File content loaded, length:", text.length);
textArea.text = text;
SessionData.notepadContent = text;
SessionData.saveSettings();
console.log("Notepad: File loaded and saved to session");
}
}
onExited: (exitCode) => {
console.log("Notepad: File read process exited with code:", exitCode);
if (exitCode !== 0) {
console.warn("Notepad: Failed to load file, exit code:", exitCode);
}
}
}
}
}
}
-138
View File
@@ -1,138 +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()
onShouldBeVisibleChanged: (shouldBeVisible) => {
if (!shouldBeVisible) {
notificationModalOpen = false
modalKeyboardController.reset()
NotificationService.onOverlayClose()
}
}
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
}
-337
View File
@@ -1,337 +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 int selectedIndex: 0
property int optionCount: 4
signal powerActionRequested(string action, string title, string message)
function selectOption() {
close()
switch (selectedIndex) {
case 0:
root.powerActionRequested("logout", "Log Out", "Are you sure you want to log out?")
break
case 1:
root.powerActionRequested("suspend", "Suspend", "Are you sure you want to suspend the system?")
break
case 2:
root.powerActionRequested("reboot", "Reboot", "Are you sure you want to reboot the system?")
break
case 3:
root.powerActionRequested("poweroff", "Power Off", "Are you sure you want to power off the system?")
break
}
}
shouldBeVisible: false
width: 320
height: 300
enableShadow: true
onBackgroundClicked: {
close()
}
onOpened: {
selectedIndex = 0
modalFocusScope.forceActiveFocus()
}
modalFocusScope.Keys.onPressed: function(event) {
switch (event.key) {
case Qt.Key_Up:
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
event.accepted = true
break
case Qt.Key_Down:
selectedIndex = (selectedIndex + 1) % optionCount
event.accepted = true
break
case Qt.Key_Tab:
selectedIndex = (selectedIndex + 1) % optionCount
event.accepted = true
break
case Qt.Key_Return:
case Qt.Key_Enter:
selectOption()
event.accepted = true
break
}
}
content: Component {
Item {
anchors.fill: parent
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: {
close()
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
if (selectedIndex === 0)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
else if (logoutArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
else
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
}
border.color: selectedIndex === 0 ? Theme.primary : "transparent"
border.width: selectedIndex === 0 ? 1 : 0
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: {
selectedIndex = 0
selectOption()
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
if (selectedIndex === 1)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
else if (suspendArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
else
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
}
border.color: selectedIndex === 1 ? Theme.primary : "transparent"
border.width: selectedIndex === 1 ? 1 : 0
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: {
selectedIndex = 1
selectOption()
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
if (selectedIndex === 2)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
else if (rebootArea.containsMouse)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08);
else
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
}
border.color: selectedIndex === 2 ? Theme.warning : "transparent"
border.width: selectedIndex === 2 ? 1 : 0
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: {
selectedIndex = 2
selectOption()
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
if (selectedIndex === 3)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
else if (powerOffArea.containsMouse)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08);
else
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
}
border.color: selectedIndex === 3 ? Theme.error : "transparent"
border.width: selectedIndex === 3 ? 1 : 0
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: {
selectedIndex = 3
selectOption()
}
}
}
}
Item {
height: Theme.spacingS
}
StyledText {
text: "↑↓ Navigate • Tab Cycle • Enter Select • Esc Close"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
opacity: 0.7
}
}
}
}
}
-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
}
}
}
}
}
}
}
}
-606
View File
@@ -1,606 +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"
}
IpcHandler {
function browse(type: string) {
if (type === "wallpaper") {
wallpaperBrowser.allowStacking = false
wallpaperBrowser.open()
} else if (type === "profile") {
profileBrowser.allowStacking = false
profileBrowser.open()
}
}
target: "file"
}
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
}
}
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": "Displays",
"icon": "monitor"
}, {
"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: displaysLoader
anchors.fill: parent
active: sidebarContainer.currentIndex === 6
visible: active
asynchronous: true
sourceComponent: DisplaysTab {}
}
Loader {
id: recentAppsLoader
anchors.fill: parent
active: sidebarContainer.currentIndex === 7
visible: active
asynchronous: true
sourceComponent: RecentAppsTab {}
}
Loader {
id: themeColorsLoader
anchors.fill: parent
active: sidebarContainer.currentIndex === 8
visible: active
asynchronous: true
sourceComponent: ThemeColorsTab {}
}
Loader {
id: aboutLoader
anchors.fill: parent
active: sidebarContainer.currentIndex === 9
visible: active
asynchronous: true
sourceComponent: AboutTab {}
}
}
}
}
}
}
}
FileBrowserModal {
id: profileBrowser
allowStacking: true
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
})
}
allowStacking = true
}
}
FileBrowserModal {
id: wallpaperBrowser
allowStacking: true
browserTitle: "Select Wallpaper"
browserIcon: "wallpaper"
browserType: "wallpaper"
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: path => {
SessionData.setWallpaper(path)
close()
}
onDialogClosed: {
allowStacking = true
}
}
}
-914
View File
@@ -1,914 +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: Quickshell.iconPath(model.icon, 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: Quickshell.iconPath(model.icon, true)
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
}
}
}
}
}
}
}
}
}
-945
View File
@@ -1,945 +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: Quickshell.iconPath(model.icon, true)
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: Quickshell.iconPath(model.icon, true)
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)
}
}
}
}
}
}
}
-284
View File
@@ -1,284 +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: displayDate.toLocaleDateString(Qt.locale(), "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: {
var days = []
var locale = Qt.locale()
for (var i = 0; i < 7; i++) {
var date = new Date(2024, 0, 7 + i)
days.push(locale.dayName(i, Locale.ShortFormat))
}
return days
}
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 ? "HH: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
}
}
}
-502
View File
@@ -1,502 +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
property real displayPosition: currentPosition
function ratio() {
if (!activePlayer || activePlayer.length <= 0) {
return 0
}
let calculatedRatio = displayPosition / activePlayer.length
return Math.max(0, Math.min(1, calculatedRatio))
}
onActivePlayerChanged: {
if (activePlayer && activePlayer.positionSupported) {
currentPosition = Qt.binding(() => activePlayer.position)
}
}
Timer {
id: positionTimer
interval: 300
running: activePlayer
&& activePlayer.playbackState === MprisPlaybackState.Playing
&& !progressMouseArea.isSeeking
repeat: true
onTriggered: 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: cleanupTimer
interval: 2000
running: !activePlayer
onTriggered: {
lastValidTitle = ""
lastValidArtist = ""
lastValidAlbum = ""
lastValidArtUrl = ""
currentPosition = 0
stop()
}
}
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
&& albumArt.status !== Image.Error)
lastValidArtUrl = activePlayer.trackArtUrl
}
fillMode: Image.PreserveAspectCrop
smooth: true
cache: true
asynchronous: true
onStatusChanged: {
if (status === Image.Error) {
console.warn("Failed to load album art:", source)
source = ""
if (activePlayer && activePlayer.trackArtUrl === source) {
lastValidArtUrl = ""
}
}
}
}
Rectangle {
anchors.fill: parent
visible: albumArt.status !== Image.Ready || !albumArt.source
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
property real pendingSeekPosition: -1
Timer {
id: seekDebounceTimer
interval: 150
repeat: false
onTriggered: {
if (progressMouseArea.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
let clampedPosition = Math.min(progressMouseArea.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clampedPosition
progressMouseArea.pendingSeekPosition = -1
}
}
}
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))
pendingSeekPosition = ratio * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
}
}
onReleased: {
isSeeking = false
seekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
let clampedPosition = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clampedPosition
pendingSeekPosition = -1
}
displayPosition = Qt.binding(() => currentPosition)
}
onPositionChanged: function (mouse) {
if (pressed && isSeeking && activePlayer
&& activePlayer.length > 0
&& activePlayer.canSeek) {
let ratio = Math.max(
0, Math.min(
1,
mouse.x / progressBarBackground.width))
pendingSeekPosition = ratio * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
}
}
onClicked: function (mouse) {
if (activePlayer && activePlayer.length > 0
&& activePlayer.canSeek) {
let ratio = Math.max(
0, Math.min(
1,
mouse.x / progressBarBackground.width))
activePlayer.position = ratio * activePlayer.length
}
}
}
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))
progressMouseArea.pendingSeekPosition = ratio * activePlayer.length
displayPosition = progressMouseArea.pendingSeekPosition
seekDebounceTimer.restart()
}
}
onReleased: {
progressMouseArea.isSeeking = false
seekDebounceTimer.stop()
if (progressMouseArea.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
let clampedPosition = Math.min(progressMouseArea.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clampedPosition
progressMouseArea.pendingSeekPosition = -1
}
displayPosition = Qt.binding(() => currentPosition)
}
}
}
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,781 +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.Modules.ControlCenter.Widgets
import qs.Modules.ControlCenter.Details
import qs.Modules.ControlCenter.Details 1.0 as Details
import qs.Services
import qs.Widgets
DankPopout {
id: root
property string expandedSection: ""
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 openWithSection(section) {
if (shouldBeVisible) {
close()
} else {
expandedSection = section
open()
}
}
function toggleSection(section) {
if (expandedSection === section) {
expandedSection = ""
} else {
expandedSection = section
}
}
signal powerActionRequested(string action, string title, string message)
signal lockRequested
popupWidth: 550
popupHeight: Math.min(Screen.height - 100, contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400)
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 {
Item {
implicitHeight: controlContent.implicitHeight
property alias bluetoothCodecSelector: bluetoothCodecSelector
Rectangle {
id: controlContent
anchors.fill: parent
implicitHeight: mainColumn.implicitHeight + Theme.spacingM
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
}
Column {
id: mainColumn
width: parent.width - Theme.spacingL * 2
x: Theme.spacingL
y: Theme.spacingL
spacing: Theme.spacingS
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
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
}
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: (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()
}
}
DankActionButton {
buttonSize: 40
iconName: root.powerOptionsExpanded ? "expand_less" : "power_settings_new"
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.powerOptionsExpanded = !root.powerOptionsExpanded
}
}
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()
}
}
}
}
Item {
width: parent.width
implicitHeight: root.powerOptionsExpanded ? 60 : 0
height: implicitHeight
clip: true
Rectangle {
width: parent.width
height: 60
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.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: "logout"
size: Theme.fontSizeSmall
color: logoutButton.containsMouse ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Logout"
font.pixelSize: Theme.fontSizeSmall
color: logoutButton.containsMouse ? Theme.primary : 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?")
}
}
}
Rectangle {
width: 100
height: 34
radius: Theme.cornerRadius
color: rebootButton.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: "restart_alt"
size: Theme.fontSizeSmall
color: rebootButton.containsMouse ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Restart"
font.pixelSize: Theme.fontSizeSmall
color: rebootButton.containsMouse ? Theme.primary : 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?")
}
}
}
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?")
}
}
}
Rectangle {
width: 100
height: 34
radius: Theme.cornerRadius
color: shutdownButton.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: "power_settings_new"
size: Theme.fontSizeSmall
color: shutdownButton.containsMouse ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Shutdown"
font.pixelSize: Theme.fontSizeSmall
color: shutdownButton.containsMouse ? Theme.primary : 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?")
}
}
}
}
}
}
Item {
width: parent.width
height: audioSliderRow.implicitHeight
Row {
id: audioSliderRow
x: -Theme.spacingS
width: parent.width + Theme.spacingS * 2
spacing: Theme.spacingM
AudioSliderRow {
width: (parent.width - Theme.spacingM) / 2
}
Item {
width: (parent.width - Theme.spacingM) / 2
height: parent.height
BrightnessSliderRow {
width: parent.width
height: parent.height
x: -Theme.spacingS
}
}
}
}
Row {
width: parent.width
spacing: Theme.spacingM
NetworkPill {
width: (parent.width - Theme.spacingM) / 2
expanded: root.expandedSection === "network"
onClicked: {
if (NetworkService.wifiToggling) {
return
}
if (NetworkService.networkStatus === "ethernet") {
if (NetworkService.ethernetConnected && !NetworkService.wifiEnabled) {
NetworkService.toggleWifiRadio()
return
}
root.toggleSection("network")
return
}
if (NetworkService.networkStatus === "wifi") {
if (NetworkService.ethernetConnected) {
NetworkService.toggleWifiRadio()
return
}
NetworkService.disconnectWifi()
return
}
if (!NetworkService.wifiEnabled) {
NetworkService.toggleWifiRadio()
return
}
if (NetworkService.wifiEnabled && NetworkService.networkStatus === "disconnected") {
root.toggleSection("network")
}
}
onExpandClicked: root.toggleSection("network")
}
BluetoothPill {
width: (parent.width - Theme.spacingM) / 2
expanded: root.expandedSection === "bluetooth"
onClicked: {
if (BluetoothService.adapter)
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled
}
onExpandClicked: root.toggleSection("bluetooth")
visible: BluetoothService.available
}
}
Loader {
width: parent.width
active: root.expandedSection === "network" || root.expandedSection === "bluetooth"
visible: active
sourceComponent: DetailView {
width: parent.width
isVisible: true
title: {
switch (root.expandedSection) {
case "network": return "Network Settings"
case "bluetooth": return "Bluetooth Settings"
default: return ""
}
}
content: {
switch (root.expandedSection) {
case "network": return networkDetailComponent
case "bluetooth": return bluetoothDetailComponent
default: return null
}
}
contentHeight: 250
}
}
Row {
width: parent.width
spacing: Theme.spacingM
AudioOutputPill {
width: (parent.width - Theme.spacingM) / 2
expanded: root.expandedSection === "audio_output"
onClicked: {
if (AudioService.sink) {
AudioService.sink.audio.muted = !AudioService.sink.audio.muted
}
}
onExpandClicked: root.toggleSection("audio_output")
}
AudioInputPill {
width: (parent.width - Theme.spacingM) / 2
expanded: root.expandedSection === "audio_input"
onClicked: {
if (AudioService.source) {
AudioService.source.audio.muted = !AudioService.source.audio.muted
}
}
onExpandClicked: root.toggleSection("audio_input")
}
}
Loader {
width: parent.width
active: root.expandedSection === "audio_output" || root.expandedSection === "audio_input"
visible: active
sourceComponent: DetailView {
width: parent.width
isVisible: true
title: {
switch (root.expandedSection) {
case "audio_output": return "Audio Output"
case "audio_input": return "Audio Input"
default: return ""
}
}
content: {
switch (root.expandedSection) {
case "audio_output": return audioOutputDetailComponent
case "audio_input": return audioInputDetailComponent
default: return null
}
}
contentHeight: 250
}
}
Row {
width: parent.width
spacing: Theme.spacingM
ToggleButton {
width: (parent.width - Theme.spacingM) / 2
iconName: DisplayService.nightModeEnabled ? "nightlight" : "dark_mode"
text: "Night Mode"
secondaryText: SessionData.nightModeAutoEnabled ? "Auto" : (DisplayService.nightModeEnabled ? "On" : "Off")
isActive: DisplayService.nightModeEnabled
enabled: DisplayService.automationAvailable
onClicked: DisplayService.toggleNightMode()
DankIcon {
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
name: "schedule"
size: 12
color: Theme.primary
visible: SessionData.nightModeAutoEnabled
opacity: 0.8
}
}
ToggleButton {
width: (parent.width - Theme.spacingM) / 2
iconName: SessionData.isLightMode ? "light_mode" : "palette"
text: "Theme"
secondaryText: SessionData.isLightMode ? "Light" : "Dark"
isActive: true
onClicked: Theme.toggleLightMode()
}
}
}
}
Details.BluetoothCodecSelector {
id: bluetoothCodecSelector
anchors.fill: parent
z: 10000
}
}
}
Component {
id: networkDetailComponent
NetworkDetail {}
}
Component {
id: bluetoothDetailComponent
BluetoothDetail {
id: bluetoothDetail
onShowCodecSelector: function(device) {
if (contentLoader.item && contentLoader.item.bluetoothCodecSelector) {
contentLoader.item.bluetoothCodecSelector.show(device)
contentLoader.item.bluetoothCodecSelector.codecSelected.connect(function(deviceAddress, codecName) {
bluetoothDetail.updateDeviceCodecDisplay(deviceAddress, codecName)
})
}
}
}
}
Component {
id: audioOutputDetailComponent
AudioOutputDetail {}
}
Component {
id: audioInputDetailComponent
AudioInputDetail {}
}
}
@@ -1,178 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
implicitHeight: headerRow.height + volumeSlider.height + audioContent.height + Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Row {
id: headerRow
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
height: 40
StyledText {
id: headerText
text: "Input Devices"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
DankSlider {
id: volumeSlider
anchors.left: parent.left
anchors.right: parent.right
anchors.top: headerRow.bottom
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingXS
height: 35
value: AudioService.source && AudioService.source.audio ? Math.round(AudioService.source.audio.volume * 100) : 0
minimum: 0
maximum: 100
leftIcon: {
if (!AudioService.source || !AudioService.source.audio) return "mic_off"
let muted = AudioService.source.audio.muted
return muted ? "mic_off" : "mic"
}
rightIcon: "volume_up"
enabled: AudioService.source && AudioService.source.audio
unit: "%"
showValue: true
visible: AudioService.source && AudioService.source.audio
onSliderValueChanged: function(newValue) {
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.volume = newValue / 100
}
}
MouseArea {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: Theme.iconSize + Theme.spacingS * 2
height: parent.height
onClicked: {
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.muted = !AudioService.source.audio.muted
}
}
}
}
DankFlickable {
id: audioContent
anchors.top: volumeSlider.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingS
contentHeight: audioColumn.height
clip: true
Column {
id: audioColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
model: Pipewire.nodes.values.filter(node => {
return node.audio && !node.isSink && !node.isStream
})
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 50
radius: Theme.cornerRadius
color: deviceMouseArea.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, index % 2 === 0 ? 0.3 : 0.2)
border.color: modelData === AudioService.source ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: modelData === AudioService.source ? 2 : 1
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: {
if (modelData.name.includes("bluez"))
return "headset"
else if (modelData.name.includes("usb"))
return "headset"
else
return "mic"
}
size: Theme.iconSize - 4
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData === AudioService.source ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
StyledText {
text: modelData === AudioService.source ? "Active" : "Available"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
}
}
MouseArea {
id: deviceMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData) {
Pipewire.preferredDefaultAudioSource = modelData
}
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
Behavior on border.color {
ColorAnimation { duration: Theme.shortDuration }
}
}
}
}
}
}
@@ -1,138 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
implicitHeight: headerRow.height + audioContent.height + Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Row {
id: headerRow
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
height: 40
StyledText {
id: headerText
text: "Audio Devices"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
DankFlickable {
id: audioContent
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
contentHeight: audioColumn.height
clip: true
Column {
id: audioColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
model: Pipewire.nodes.values.filter(node => {
return node.audio && node.isSink && !node.isStream
})
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 50
radius: Theme.cornerRadius
color: deviceMouseArea.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, index % 2 === 0 ? 0.3 : 0.2)
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: modelData === AudioService.sink ? 2 : 1
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
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 - 4
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
StyledText {
text: modelData === AudioService.sink ? "Active" : "Available"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
}
}
MouseArea {
id: deviceMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData) {
Pipewire.preferredDefaultAudioSink = modelData
}
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
Behavior on border.color {
ColorAnimation { duration: Theme.shortDuration }
}
}
}
}
}
}
@@ -1,307 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var device: null
property bool modalVisible: false
property var parentItem
property var availableCodecs: []
property string currentCodec: ""
property bool isLoading: false
signal codecSelected(string deviceAddress, string codecName)
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;
BluetoothService.getAvailableCodecs(device, function(codecs, current) {
availableCodecs = codecs;
currentCodec = current;
isLoading = false;
});
}
function selectCodec(profileName) {
if (!device || isLoading)
return;
let selectedCodec = availableCodecs.find(c => c.profile === profileName);
if (selectedCodec && device) {
BluetoothService.updateDeviceCodec(device.address, selectedCodec.name);
codecSelected(device.address, selectedCodec.name);
}
isLoading = true;
BluetoothService.switchCodec(device, profileName, function(success, message) {
isLoading = false;
if (success) {
ToastService.showToast(message, ToastService.levelInfo);
Qt.callLater(root.hide);
} else {
ToastService.showToast(message, ToastService.levelError);
}
});
}
visible: false
anchors.fill: parent
z: 2000
MouseArea {
id: modalBlocker
anchors.fill: parent
visible: modalVisible
enabled: modalVisible
hoverEnabled: true
preventStealing: true
propagateComposedEvents: false
onClicked: root.hide()
onWheel: (wheel) => { wheel.accepted = true }
onPositionChanged: (mouse) => { mouse.accepted = true }
}
Rectangle {
id: modalBackground
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5)
opacity: modalVisible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
FocusScope {
id: focusScope
anchors.fill: parent
focus: root.visible
enabled: root.visible
Keys.onEscapePressed: {
root.hide()
event.accepted = true
}
}
Rectangle {
id: modalContent
anchors.centerIn: parent
width: 320
height: contentColumn.implicitHeight + Theme.spacingL * 2
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
hoverEnabled: true
preventStealing: true
propagateComposedEvents: false
onClicked: (mouse) => { mouse.accepted = true }
onWheel: (wheel) => { wheel.accepted = true }
onPositionChanged: (mouse) => { mouse.accepted = true }
}
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
}
}
}
}
@@ -1,543 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
implicitHeight: BluetoothService.adapter && BluetoothService.adapter.enabled ? headerRow.height + bluetoothContent.height + Theme.spacingM : headerRow.height
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
property var bluetoothCodecModalRef: null
signal showCodecSelector(var device)
function updateDeviceCodecDisplay(deviceAddress, codecName) {
for (let i = 0; i < pairedRepeater.count; i++) {
let item = pairedRepeater.itemAt(i)
if (item && item.modelData && item.modelData.address === deviceAddress) {
item.currentCodec = codecName
break
}
}
}
Row {
id: headerRow
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
height: 40
StyledText {
id: headerText
text: "Bluetooth Settings"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: Math.max(0, parent.width - headerText.implicitWidth - scanButton.width - Theme.spacingM)
height: parent.height
}
Rectangle {
id: scanButton
width: 100
height: 36
radius: 18
color: {
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
return scanMouseArea.containsMouse ? Theme.surfaceContainerHigh : "transparent"
}
border.color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching"
size: 18
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: BluetoothService.adapter && BluetoothService.adapter.discovering ? "Scanning" : "Scan"
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: scanMouseArea
anchors.fill: parent
hoverEnabled: true
enabled: BluetoothService.adapter && BluetoothService.adapter.enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (BluetoothService.adapter)
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
DankFlickable {
id: bluetoothContent
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
contentHeight: bluetoothColumn.height
clip: true
Column {
id: bluetoothColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
id: pairedRepeater
model: {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return []
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
devices.sort((a, b) => {
if (a.connected && !b.connected) return -1
if (!a.connected && b.connected) return 1
return (b.signalStrength || 0) - (a.signalStrength || 0)
})
return devices
}
delegate: Rectangle {
required property var modelData
required property int index
property string currentCodec: BluetoothService.deviceCodecs[modelData.address] || ""
width: parent.width
height: 50
radius: Theme.cornerRadius
Component.onCompleted: {
if (modelData.connected && BluetoothService.isAudioDevice(modelData)) {
BluetoothService.refreshDeviceCodec(modelData)
}
}
color: {
if (modelData.state === BluetoothDeviceState.Connecting)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
if (deviceMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
}
border.color: {
if (modelData.state === BluetoothDeviceState.Connecting)
return Theme.warning
if (modelData.connected)
return Theme.primary
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
border.width: (modelData.connected || modelData.state === BluetoothDeviceState.Connecting) ? 2 : 1
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: BluetoothService.getDeviceIcon(modelData)
size: Theme.iconSize - 4
color: {
if (modelData.state === BluetoothDeviceState.Connecting)
return Theme.warning
if (modelData.connected)
return Theme.primary
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: 200
StyledText {
text: modelData.name || modelData.deviceName || "Unknown Device"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData.connected ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
}
Row {
spacing: Theme.spacingXS
StyledText {
text: {
if (modelData.state === BluetoothDeviceState.Connecting)
return "Connecting..."
if (modelData.connected) {
let status = "Connected"
if (currentCodec) {
status += " • " + currentCodec
}
return status
}
return "Paired"
}
font.pixelSize: Theme.fontSizeSmall
color: {
if (modelData.state === BluetoothDeviceState.Connecting)
return Theme.warning
return Theme.surfaceVariantText
}
}
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: Theme.surfaceVariantText
visible: text.length > 0
}
StyledText {
text: modelData.signalStrength !== undefined && modelData.signalStrength > 0 ? "• " + modelData.signalStrength + "%" : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: text.length > 0
}
}
}
}
DankActionButton {
id: pairedOptionsButton
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
iconName: "more_horiz"
buttonSize: 28
onClicked: {
if (bluetoothContextMenu.visible) {
bluetoothContextMenu.close()
} else {
bluetoothContextMenu.currentDevice = modelData
bluetoothContextMenu.popup(pairedOptionsButton, -bluetoothContextMenu.width + pairedOptionsButton.width, pairedOptionsButton.height + Theme.spacingXS)
}
}
}
MouseArea {
id: deviceMouseArea
anchors.fill: parent
anchors.rightMargin: pairedOptionsButton.width + Theme.spacingS
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.connected) {
modelData.disconnect()
} else {
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
Behavior on border.color {
ColorAnimation { duration: Theme.shortDuration }
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
visible: pairedRepeater.count > 0 && availableRepeater.count > 0
}
Item {
width: parent.width
height: 80
visible: BluetoothService.adapter && BluetoothService.adapter.discovering && availableRepeater.count === 0
DankIcon {
anchors.centerIn: parent
name: "sync"
size: 24
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.4)
RotationAnimation on rotation {
running: parent.visible && BluetoothService.adapter && BluetoothService.adapter.discovering && availableRepeater.count === 0
loops: Animation.Infinite
from: 0
to: 360
duration: 1500
}
}
}
Repeater {
id: availableRepeater
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)
}
delegate: Rectangle {
required property var modelData
required property int index
property bool canConnect: BluetoothService.canConnect(modelData)
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
width: parent.width
height: 50
radius: Theme.cornerRadius
color: availableMouseArea.containsMouse && !isBusy ? 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.15)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
opacity: canConnect ? 1 : 0.6
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: BluetoothService.getDeviceIcon(modelData)
size: Theme.iconSize - 4
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: 200
StyledText {
text: modelData.name || modelData.deviceName || "Unknown Device"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
elide: Text.ElideRight
width: parent.width
}
Row {
spacing: Theme.spacingXS
StyledText {
text: {
if (modelData.pairing) return "Pairing..."
if (modelData.blocked) return "Blocked"
return BluetoothService.getSignalStrength(modelData)
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: modelData.signalStrength !== undefined && modelData.signalStrength > 0 ? "• " + modelData.signalStrength + "%" : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: text.length > 0 && !modelData.pairing && !modelData.blocked
}
}
}
}
StyledText {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: {
if (modelData.pairing) return "Pairing..."
if (!canConnect) return "Cannot pair"
return "Pair"
}
font.pixelSize: Theme.fontSizeSmall
color: canConnect ? Theme.primary : Theme.surfaceVariantText
font.weight: Font.Medium
}
MouseArea {
id: availableMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: canConnect && !isBusy
onClicked: {
if (modelData) {
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
}
}
Item {
width: parent.width
height: 60
visible: !BluetoothService.adapter
StyledText {
anchors.centerIn: parent
text: "No Bluetooth adapter found"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
}
}
}
Menu {
id: bluetoothContextMenu
width: 150
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
property var currentDevice: null
background: Rectangle {
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.width: 1
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
MenuItem {
text: bluetoothContextMenu.currentDevice && bluetoothContextMenu.currentDevice.connected ? "Disconnect" : "Connect"
height: 32
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
if (bluetoothContextMenu.currentDevice) {
if (bluetoothContextMenu.currentDevice.connected) {
bluetoothContextMenu.currentDevice.disconnect()
} else {
BluetoothService.connectDeviceWithTrust(bluetoothContextMenu.currentDevice)
}
}
}
}
MenuItem {
text: "Audio Codec"
height: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected ? 32 : 0
visible: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
if (bluetoothContextMenu.currentDevice) {
showCodecSelector(bluetoothContextMenu.currentDevice)
}
}
}
MenuItem {
text: "Forget Device"
height: 32
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
if (bluetoothContextMenu.currentDevice) {
bluetoothContextMenu.currentDevice.forget()
}
}
}
}
}
@@ -1,533 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modals
Rectangle {
implicitHeight: {
if (NetworkService.wifiToggling) {
return headerRow.height + wifiToggleContent.height + Theme.spacingM
}
if (NetworkService.wifiEnabled) {
return headerRow.height + wifiContent.height + Theme.spacingM
}
return headerRow.height + wifiOffContent.height + Theme.spacingM
}
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Component.onCompleted: {
NetworkService.addRef()
if (NetworkService.wifiEnabled) {
NetworkService.scanWifi()
}
}
Component.onDestruction: {
NetworkService.removeRef()
}
property var wifiPasswordModalRef: {
wifiPasswordModalLoader.active = true
return wifiPasswordModalLoader.item
}
property var networkInfoModalRef: {
networkInfoModalLoader.active = true
return networkInfoModalLoader.item
}
Row {
id: headerRow
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
height: 40
StyledText {
id: headerText
text: "Network Settings"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: Math.max(0, parent.width - headerText.implicitWidth - preferenceControls.width - Theme.spacingM)
height: parent.height
}
Row {
id: preferenceControls
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.ethernetConnected && NetworkService.wifiConnected
Rectangle {
property bool isActive: NetworkService.userPreference === "ethernet"
width: 90
height: 36
radius: 18
color: isActive ? Theme.surfaceContainerHigh : ethernetMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
StyledText {
anchors.centerIn: parent
text: "Ethernet"
color: parent.isActive ? Theme.primary : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: parent.isActive ? Font.Medium : Font.Normal
}
MouseArea {
id: ethernetMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: NetworkService.setNetworkPreference("ethernet")
cursorShape: Qt.PointingHandCursor
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
property bool isActive: NetworkService.userPreference === "wifi"
width: 70
height: 36
radius: 18
color: isActive ? Theme.surfaceContainerHigh : wifiMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
StyledText {
anchors.centerIn: parent
text: "WiFi"
color: parent.isActive ? Theme.primary : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: parent.isActive ? Font.Medium : Font.Normal
}
MouseArea {
id: wifiMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: NetworkService.setNetworkPreference("wifi")
cursorShape: Qt.PointingHandCursor
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
Item {
id: wifiToggleContent
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: NetworkService.wifiToggling
height: visible ? 80 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "sync"
size: 32
color: Theme.primary
RotationAnimation on rotation {
running: NetworkService.wifiToggling
loops: Animation.Infinite
from: 0
to: 360
duration: 1000
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
}
}
}
Item {
id: wifiOffContent
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: !NetworkService.wifiEnabled && !NetworkService.wifiToggling
height: visible ? 120 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingL
width: parent.width
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "wifi_off"
size: 48
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: "WiFi is off"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
}
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
width: 120
height: 36
radius: 18
color: enableWifiButton.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.width: 1
border.color: Theme.primary
StyledText {
anchors.centerIn: parent
text: "Enable WiFi"
color: Theme.primary
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
}
MouseArea {
id: enableWifiButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NetworkService.toggleWifiRadio()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
DankFlickable {
id: wifiContent
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: NetworkService.wifiInterface && NetworkService.wifiEnabled && !NetworkService.wifiToggling
contentHeight: wifiColumn.height
clip: true
Column {
id: wifiColumn
width: parent.width
spacing: Theme.spacingS
Item {
width: parent.width
height: 200
visible: NetworkService.wifiInterface && NetworkService.wifiNetworks?.length < 1 && !NetworkService.wifiToggling
DankIcon {
anchors.centerIn: parent
name: "refresh"
size: 48
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.3)
RotationAnimation on rotation {
running: true
loops: Animation.Infinite
from: 0
to: 360
duration: 1000
}
}
}
Repeater {
model: {
let networks = [...NetworkService.wifiNetworks]
networks.sort((a, b) => {
if (a.ssid === NetworkService.currentWifiSSID) return -1
if (b.ssid === NetworkService.currentWifiSSID) return 1
return b.signal - a.signal
})
return networks
}
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 50
radius: Theme.cornerRadius
color: networkMouseArea.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, index % 2 === 0 ? 0.3 : 0.2)
border.color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: modelData.ssid === NetworkService.currentWifiSSID ? 2 : 1
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: {
let strength = modelData.signal || 0
if (strength >= 70) return "signal_wifi_4_bar"
if (strength >= 50) return "network_wifi_3_bar"
if (strength >= 25) return "network_wifi_2_bar"
if (strength >= 10) return "network_wifi_1_bar"
return "signal_wifi_bad"
}
size: Theme.iconSize - 4
color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: 200
StyledText {
text: modelData.ssid || "Unknown Network"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData.ssid === NetworkService.currentWifiSSID ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
}
Row {
spacing: Theme.spacingXS
StyledText {
text: modelData.ssid === NetworkService.currentWifiSSID ? "Connected" : (modelData.secured ? "Secured" : "Open")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: modelData.saved ? "• Saved" : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
visible: text.length > 0
}
StyledText {
text: "• " + modelData.signal + "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
DankActionButton {
id: optionsButton
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
iconName: "more_horiz"
buttonSize: 28
onClicked: {
if (networkContextMenu.visible) {
networkContextMenu.close()
} else {
networkContextMenu.currentSSID = modelData.ssid
networkContextMenu.currentSecured = modelData.secured
networkContextMenu.currentConnected = modelData.ssid === NetworkService.currentWifiSSID
networkContextMenu.currentSaved = modelData.saved
networkContextMenu.currentSignal = modelData.signal
networkContextMenu.popup(optionsButton, -networkContextMenu.width + optionsButton.width, optionsButton.height + Theme.spacingXS)
}
}
}
MouseArea {
id: networkMouseArea
anchors.fill: parent
anchors.rightMargin: optionsButton.width + Theme.spacingS
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: function(event) {
if (modelData.ssid !== NetworkService.currentWifiSSID) {
if (modelData.secured && !modelData.saved) {
if (wifiPasswordModalRef) {
wifiPasswordModalRef.show(modelData.ssid)
}
} else {
NetworkService.connectToWifi(modelData.ssid)
}
}
event.accepted = true
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
Behavior on border.color {
ColorAnimation { duration: Theme.shortDuration }
}
}
}
}
}
Menu {
id: networkContextMenu
width: 150
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
property string currentSSID: ""
property bool currentSecured: false
property bool currentConnected: false
property bool currentSaved: false
property int currentSignal: 0
background: Rectangle {
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.width: 1
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
MenuItem {
text: networkContextMenu.currentConnected ? "Disconnect" : "Connect"
height: 32
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
if (networkContextMenu.currentConnected) {
NetworkService.disconnectWifi()
} else {
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
if (wifiPasswordModalRef) {
wifiPasswordModalRef.show(networkContextMenu.currentSSID)
}
} else {
NetworkService.connectToWifi(networkContextMenu.currentSSID)
}
}
}
}
MenuItem {
text: "Network Info"
height: 32
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
if (networkInfoModalRef) {
let networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID)
networkInfoModalRef.showNetworkInfo(networkContextMenu.currentSSID, networkData)
}
}
}
MenuItem {
text: "Forget Network"
height: networkContextMenu.currentSaved || networkContextMenu.currentConnected ? 32 : 0
visible: networkContextMenu.currentSaved || networkContextMenu.currentConnected
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
NetworkService.forgetWifiNetwork(networkContextMenu.currentSSID)
}
}
}
LazyLoader {
id: wifiPasswordModalLoader
active: false
WifiPasswordModal {
id: wifiPasswordModal
}
}
LazyLoader {
id: networkInfoModalLoader
active: false
NetworkInfoModal {
id: networkInfoModal
}
}
}
-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
}
}
}
}
@@ -1,57 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Widgets
BasePill {
id: root
property var defaultSource: AudioService.source
iconName: {
if (!defaultSource) return "mic_off"
let volume = defaultSource.audio.volume
let muted = defaultSource.audio.muted
if (muted || volume === 0.0) return "mic_off"
return "mic"
}
isActive: defaultSource && !defaultSource.audio.muted
primaryText: {
if (!defaultSource) {
return "No input device"
}
return defaultSource.description || "Audio Input"
}
secondaryText: {
if (!defaultSource) {
return "Select device"
}
if (defaultSource.audio.muted) {
return "Muted"
}
return Math.round(defaultSource.audio.volume * 100) + "%"
}
onWheelEvent: function (wheelEvent) {
if (!defaultSource || !defaultSource.audio) return
let delta = wheelEvent.angleDelta.y
let currentVolume = defaultSource.audio.volume * 100
let newVolume
if (delta > 0)
newVolume = Math.min(100, currentVolume + 5)
else
newVolume = Math.max(0, currentVolume - 5)
defaultSource.audio.muted = false
defaultSource.audio.volume = newVolume / 100
wheelEvent.accepted = true
}
}
@@ -1,60 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Widgets
BasePill {
id: root
property var defaultSink: AudioService.sink
iconName: {
if (!defaultSink) return "volume_off"
let volume = defaultSink.audio.volume
let muted = defaultSink.audio.muted
if (muted || volume === 0.0) return "volume_off"
if (volume <= 0.33) return "volume_down"
if (volume <= 0.66) return "volume_up"
return "volume_up"
}
isActive: defaultSink && !defaultSink.audio.muted
primaryText: {
if (!defaultSink) {
return "No output device"
}
return defaultSink.description || "Audio Output"
}
secondaryText: {
if (!defaultSink) {
return "Select device"
}
if (defaultSink.audio.muted) {
return "Muted"
}
return Math.round(defaultSink.audio.volume * 100) + "%"
}
onWheelEvent: function (wheelEvent) {
if (!defaultSink || !defaultSink.audio) return
let delta = wheelEvent.angleDelta.y
let currentVolume = defaultSink.audio.volume * 100
let newVolume
if (delta > 0)
newVolume = Math.min(100, currentVolume + 5)
else
newVolume = Math.max(0, currentVolume - 5)
defaultSink.audio.muted = false
defaultSink.audio.volume = newVolume / 100
AudioService.volumeChanged()
wheelEvent.accepted = true
}
}
@@ -1,50 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Widgets
SimpleSlider {
id: root
property var defaultSink: AudioService.sink
iconName: {
if (!defaultSink) return "volume_off"
let volume = defaultSink.audio.volume
let muted = defaultSink.audio.muted
if (muted || volume === 0.0) return "volume_off"
if (volume <= 0.33) return "volume_down"
if (volume <= 0.66) return "volume_up"
return "volume_up"
}
iconColor: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
enabled: defaultSink !== null
allowIconClick: defaultSink !== null
value: defaultSink ? defaultSink.audio.volume : 0.0
maximumValue: 1.0
minimumValue: 0.0
onSliderValueChanged: function(newValue) {
if (defaultSink) {
defaultSink.audio.volume = newValue
if (newValue > 0 && defaultSink.audio.muted) {
defaultSink.audio.muted = false
}
}
}
onIconClicked: function() {
if (defaultSink) {
defaultSink.audio.muted = !defaultSink.audio.muted
}
}
}
@@ -1,75 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
Row {
id: root
property var defaultSink: AudioService.sink
height: 40
spacing: Theme.spacingS
Rectangle {
width: Theme.iconSize + Theme.spacingS * 2
height: Theme.iconSize + Theme.spacingS * 2
anchors.verticalCenter: parent.verticalCenter
radius: (Theme.iconSize + Theme.spacingS * 2) / 2 // Make it circular
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
MouseArea {
id: iconArea
anchors.fill: parent
visible: defaultSink !== null
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (defaultSink) {
defaultSink.audio.muted = !defaultSink.audio.muted
}
}
}
DankIcon {
anchors.centerIn: parent
name: {
if (!defaultSink) return "volume_off"
let volume = defaultSink.audio.volume
let muted = defaultSink.audio.muted
if (muted || volume === 0.0) return "volume_off"
if (volume <= 0.33) return "volume_down"
if (volume <= 0.66) return "volume_up"
return "volume_up"
}
size: Theme.iconSize
color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
}
}
DankSlider {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (Theme.iconSize + Theme.spacingS * 2) - Theme.spacingM
enabled: defaultSink !== null
minimum: 0
maximum: 100
value: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
onSliderValueChanged: function(newValue) {
if (defaultSink) {
defaultSink.audio.volume = newValue / 100.0
if (newValue > 0 && defaultSink.audio.muted) {
defaultSink.audio.muted = false
}
}
}
}
}
-149
View File
@@ -1,149 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: root
property string iconName: ""
property color iconColor: Theme.surfaceText
property string primaryText: ""
property string secondaryText: ""
property bool expanded: false
property bool isActive: false
signal clicked()
signal expandClicked()
signal wheelEvent(var wheelEvent)
width: parent ? parent.width : 200
height: 60
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Rectangle {
id: mainArea
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width - expandArea.width
topLeftRadius: Theme.cornerRadius
bottomLeftRadius: Theme.cornerRadius
topRightRadius: 0
bottomRightRadius: 0
color: mainAreaMouse.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
"transparent"
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: root.iconName
size: Theme.iconSize
color: root.isActive ? Theme.primary : root.iconColor
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - Theme.iconSize - Theme.spacingS
spacing: 2
StyledText {
width: parent.width
text: root.primaryText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
StyledText {
width: parent.width
text: root.secondaryText
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: text.length > 0
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
MouseArea {
id: mainAreaMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
onWheel: function (wheelEvent) {
root.wheelEvent(wheelEvent)
}
}
}
Rectangle {
id: expandArea
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
width: Theme.iconSize + Theme.spacingM * 2
topLeftRadius: 0
bottomLeftRadius: 0
topRightRadius: Theme.cornerRadius
bottomRightRadius: Theme.cornerRadius
color: expandAreaMouse.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
"transparent"
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
DankIcon {
id: expandIcon
anchors.centerIn: parent
name: expanded ? "expand_less" : "expand_more"
size: Theme.iconSize - 2
color: Theme.surfaceVariantText
}
MouseArea {
id: expandAreaMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.expandClicked()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
@@ -1,66 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Widgets
BasePill {
id: root
property var primaryDevice: {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices) {
return null
}
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
for (let device of devices) {
if (device && device.connected) {
return device
}
}
return null
}
iconName: {
if (!BluetoothService.available) {
return "bluetooth_disabled"
}
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
return "bluetooth_disabled"
}
if (primaryDevice) {
return BluetoothService.getDeviceIcon(primaryDevice)
}
return "bluetooth"
}
isActive: !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled)
primaryText: {
if (!BluetoothService.available) {
return "Bluetooth unavailable"
}
if (!BluetoothService.adapter) {
return "No adapter"
}
if (!BluetoothService.adapter.enabled) {
return "Disabled"
}
return "Enabled"
}
secondaryText: {
if (!BluetoothService.available) {
return "Hardware not found"
}
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
return "Off"
}
if (primaryDevice) {
return primaryDevice.name || primaryDevice.alias || primaryDevice.deviceName || "Connected Device"
}
return "No devices"
}
}
@@ -1,34 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Widgets
SimpleSlider {
id: root
iconName: {
if (!DisplayService.brightnessAvailable) return "brightness_low"
let brightness = DisplayService.brightnessLevel
if (brightness <= 33) return "brightness_low"
if (brightness <= 66) return "brightness_medium"
return "brightness_high"
}
iconColor: DisplayService.brightnessAvailable && DisplayService.brightnessLevel > 0 ? Theme.primary : Theme.surfaceText
enabled: DisplayService.brightnessAvailable
value: DisplayService.brightnessLevel
maximumValue: 100.0
minimumValue: 0.0
onSliderValueChanged: function(newValue) {
if (DisplayService.brightnessAvailable) {
DisplayService.brightnessLevel = newValue
}
}
}
@@ -1,142 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Row {
id: root
height: 40
spacing: Theme.spacingS
Rectangle {
width: Theme.iconSize + Theme.spacingS * 2
height: Theme.iconSize + Theme.spacingS * 2
anchors.verticalCenter: parent.verticalCenter
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
color: iconArea.containsMouse
? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
: "transparent"
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
MouseArea {
id: iconArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: function(event) {
if (DisplayService.devices.length > 1) {
if (deviceMenu.visible) {
deviceMenu.close()
} else {
deviceMenu.popup(iconArea, 0, iconArea.height + Theme.spacingXS)
}
event.accepted = true
}
}
}
DankIcon {
anchors.centerIn: parent
name: {
if (!DisplayService.brightnessAvailable) return "brightness_low"
let brightness = DisplayService.brightnessLevel
if (brightness <= 33) return "brightness_low"
if (brightness <= 66) return "brightness_medium"
return "brightness_high"
}
size: Theme.iconSize
color: DisplayService.brightnessAvailable && DisplayService.brightnessLevel > 0 ? Theme.primary : Theme.surfaceText
}
}
DankSlider {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (Theme.iconSize + Theme.spacingS * 2) - Theme.spacingM
enabled: DisplayService.brightnessAvailable
minimum: 1
maximum: 100
value: {
let level = DisplayService.brightnessLevel
if (level > 100) {
let deviceInfo = DisplayService.getCurrentDeviceInfo()
if (deviceInfo && deviceInfo.max > 0) {
return Math.round((level / deviceInfo.max) * 100)
}
return 50
}
return level
}
onSliderValueChanged: function(newValue) {
if (DisplayService.brightnessAvailable) {
DisplayService.setBrightness(newValue)
}
}
}
Menu {
id: deviceMenu
width: 200
closePolicy: Popup.CloseOnEscape
background: Rectangle {
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.width: 1
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
Instantiator {
model: DisplayService.devices
delegate: MenuItem {
required property var modelData
required property int index
property string deviceName: modelData.name || ""
property string deviceClass: modelData.class || ""
text: deviceName
font.pixelSize: Theme.fontSizeMedium
height: 40
indicator: Rectangle {
visible: DisplayService.currentDevice === parent.deviceName
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingS
width: 4
height: parent.height - Theme.spacingS * 2
radius: 2
color: Theme.primary
}
contentItem: StyledText {
text: parent.text
font: parent.font
color: DisplayService.currentDevice === parent.deviceName ? Theme.primary : Theme.surfaceText
leftPadding: Theme.spacingL
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
DisplayService.setCurrentDevice(deviceName, true)
deviceMenu.close()
}
}
onObjectAdded: (index, object) => deviceMenu.insertItem(index, object)
onObjectRemoved: (index, object) => deviceMenu.removeItem(object)
}
}
}
@@ -1,70 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: root
property string iconName: ""
property color iconColor: Theme.surfaceText
property string labelText: ""
property real value: 0.0
property real maximumValue: 1.0
property real minimumValue: 0.0
property bool enabled: true
signal sliderValueChanged(real value)
width: parent ? parent.width : 200
height: 60
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: enabled ? 1.0 : 0.6
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.right: sliderContainer.left
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: root.iconName
size: Theme.iconSize
color: root.iconColor
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.labelText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Rectangle {
id: sliderContainer
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingM
width: 120
height: parent.height - Theme.spacingS * 2
DankSlider {
anchors.centerIn: parent
width: parent.width
enabled: root.enabled
minimum: Math.round(root.minimumValue * 100)
maximum: Math.round(root.maximumValue * 100)
value: Math.round(root.value * 100)
onSliderValueChanged: root.sliderValueChanged(newValue / 100.0)
}
}
}
@@ -1,29 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: root
property string title: ""
property Component content: null
property bool isVisible: true
property int contentHeight: 300
width: parent ? parent.width : 400
implicitHeight: isVisible ? contentHeight : 0
height: implicitHeight
color: "transparent"
clip: true
Loader {
id: contentLoader
anchors.fill: parent
sourceComponent: root.content
asynchronous: true
}
}
@@ -1,72 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Widgets
BasePill {
id: root
isActive: {
if (NetworkService.wifiToggling) {
return false
}
if (NetworkService.networkStatus === "ethernet") {
return true
}
if (NetworkService.networkStatus === "wifi") {
return true
}
return NetworkService.wifiEnabled
}
iconName: {
if (NetworkService.wifiToggling) {
return "sync"
}
if (NetworkService.networkStatus === "ethernet") {
return "settings_ethernet"
}
if (NetworkService.networkStatus === "wifi") {
return NetworkService.wifiSignalIcon
}
if (NetworkService.wifiEnabled) {
return "signal_wifi_off"
}
return "wifi_off"
}
primaryText: {
if (NetworkService.wifiToggling) {
return NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
}
if (NetworkService.networkStatus === "ethernet") {
return "Ethernet"
}
if (NetworkService.networkStatus === "wifi" && NetworkService.currentWifiSSID) {
return NetworkService.currentWifiSSID
}
if (NetworkService.wifiEnabled) {
return "Not connected"
}
return "WiFi off"
}
secondaryText: {
if (NetworkService.wifiToggling) {
return "Please wait..."
}
if (NetworkService.networkStatus === "ethernet") {
return "Connected"
}
if (NetworkService.networkStatus === "wifi") {
return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected"
}
if (NetworkService.wifiEnabled) {
return "Select network"
}
return "Tap to enable"
}
}
@@ -1,50 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Widgets
Row {
id: root
property string iconName: ""
property color iconColor: Theme.surfaceText
property real value: 0.0
property real maximumValue: 1.0
property real minimumValue: 0.0
property bool enabled: true
property bool allowIconClick: false
signal sliderValueChanged(real value)
signal iconClicked()
height: 60
spacing: Theme.spacingM
DankIcon {
name: root.iconName
size: Theme.iconSize
color: root.iconColor
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
visible: root.allowIconClick
cursorShape: Qt.PointingHandCursor
onClicked: root.iconClicked()
}
}
DankSlider {
anchors.verticalCenter: parent.verticalCenter
width: {
if (parent.width <= 0) return 80
return Math.max(80, Math.min(400, parent.width - Theme.iconSize - Theme.spacingM))
}
enabled: root.enabled
minimum: Math.round(root.minimumValue * 100)
maximum: Math.round(root.maximumValue * 100)
value: Math.round(root.value * 100)
onSliderValueChanged: function(newValue) { root.sliderValueChanged(newValue / 100.0) }
}
}
@@ -1,95 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: root
property string iconName: ""
property string text: ""
property bool isActive: false
property bool enabled: true
property string secondaryText: ""
signal clicked()
width: parent ? parent.width : 200
height: 60
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: enabled ? 1.0 : 0.6
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: mouseArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
"transparent"
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
}
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: root.iconName
size: Theme.iconSize
color: root.isActive ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - Theme.iconSize - Theme.spacingS
spacing: 2
StyledText {
width: parent.width
text: root.text
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
StyledText {
width: parent.width
text: root.secondaryText
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: text.length > 0
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: root.enabled
onClicked: root.clicked()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
-200
View File
@@ -1,200 +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
property real currentScreen: modelData ? modelData : dock.screen
property real screenWidth: currentScreen ? currentScreen.geometry.width : 1920
property real maxDockWidth: Math.min(screenWidth * 0.8, 1200)
height: dock.reveal ? 65 : 20
width: dock.reveal ? Math.min(dockBackground.width + 32, maxDockWidth) : Math.min(Math.max(dockBackground.width + 64, 200), screenWidth * 0.5)
anchors {
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
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
}
}
}
}
}
-350
View File
@@ -1,350 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
clip: false
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 bool isWindowFocused: {
if (!appData || appData.type !== "window") {
return false
}
var toplevel = getToplevelObject()
if (!toplevel) {
return false
}
return toplevel.activated
}
property string tooltipText: {
if (!appData)
return ""
// For window type, show app name + window title
if (appData.type === "window" && showWindowTitle) {
var desktopEntry = DesktopEntries.heuristicLookup(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.heuristicLookup(appData.appId)
return desktopEntry
&& desktopEntry.name ? desktopEntry.name : appData.appId
}
width: 40
height: 40
function getToplevelObject() {
if (!appData || appData.type !== "window") {
return null
}
var sortedToplevels = CompositorService.sortedToplevels
if (!sortedToplevels) {
return null
}
if (appData.uniqueId) {
for (var i = 0; i < sortedToplevels.length; i++) {
var toplevel = sortedToplevels[i]
var checkId = toplevel.title + "|" + (toplevel.appId || "") + "|" + i
if (checkId === appData.uniqueId) {
return toplevel
}
}
}
if (appData.windowId !== undefined && appData.windowId !== null && appData.windowId >= 0) {
if (appData.windowId < sortedToplevels.length) {
return sortedToplevels[appData.windowId]
}
}
return null
}
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.heuristicLookup(
appData.appId)
if (desktopEntry)
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": desktopEntry.name
|| appData.appId,
"icon": desktopEntry.icon
|| "",
"exec": desktopEntry.exec
|| "",
"comment": desktopEntry.comment || ""
})
desktopEntry.execute()
}
} else if (appData.type === "window") {
var toplevel = getToplevelObject()
if (toplevel) {
toplevel.activate()
}
}
} else if (mouse.button === Qt.MiddleButton) {
if (appData && appData.appId) {
var desktopEntry = DesktopEntries.heuristicLookup(
appData.appId)
if (desktopEntry)
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": desktopEntry.name
|| appData.appId,
"icon": desktopEntry.icon
|| "",
"exec": desktopEntry.exec
|| "",
"comment": desktopEntry.comment
|| ""
})
desktopEntry.execute()
}
} else if (mouse.button === Qt.RightButton) {
if (contextMenu)
contextMenu.showForButton(root, appData, 40)
}
}
}
IconImage {
id: iconImg
anchors.centerIn: parent
implicitSize: 40
source: {
if (appData.appId === "__SEPARATOR__") return ""
var desktopEntry = DesktopEntries.heuristicLookup(Paths.moddedAppId(appData.appId))
return desktopEntry && desktopEntry.icon ? Quickshell.iconPath(desktopEntry.icon, true) : ""
}
mipmap: true
smooth: true
asynchronous: true
visible: status === Image.Ready
}
Rectangle {
width: 40
height: 40
anchors.centerIn: parent
visible: iconImg.status !== Image.Ready
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.heuristicLookup(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 (isWindowFocused)
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
}
}
-170
View File
@@ -1,170 +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, index) => {
// Limit window title length for tooltip
var title = toplevel.title || "(Unnamed)"
if (title.length > 50) {
title = title.substring(0, 47) + "..."
}
var uniqueId = toplevel.title + "|" + (toplevel.appId || "") + "|" + index
items.push({
"type": "window",
"appId": toplevel.appId,
"windowId": index,
"windowTitle": title,
"workspaceId": -1,
"isPinned": false,
"isRunning": true,
"uniqueId": uniqueId
})
})
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
}
ConfirmModal {
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"
ConfirmModal {
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, true)
}
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, true)
}
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, true)
}
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 = DisplayService.getCurrentDeviceInfo()
return (deviceInfo && deviceInfo.class === "ddc") ? 200 : 50
}
repeat: false
onTriggered: {
DisplayService.setBrightnessInternal(pendingValue, DisplayService.lastIpcDevice)
}
}
Connections {
target: DisplayService
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 = DisplayService.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: DisplayService.brightnessAvailable
showValue: true
unit: "%"
Component.onCompleted: {
if (DisplayService.brightnessAvailable)
value = DisplayService.brightnessLevel
}
onSliderValueChanged: function(newValue) {
if (DisplayService.brightnessAvailable) {
root.brightnessDebounceTimer.pendingValue = newValue
root.brightnessDebounceTimer.restart()
resetHideTimer()
}
}
onContainsMouseChanged: {
setChildHovered(containsMouse)
}
onSliderDragFinished: function(finalValue) {
if (DisplayService.brightnessAvailable) {
root.brightnessDebounceTimer.stop()
DisplayService.setBrightnessInternal(finalValue, DisplayService.lastIpcDevice)
}
}
Connections {
target: DisplayService
function onBrightnessChanged() {
if (!brightnessSlider.pressed)
brightnessSlider.value = DisplayService.brightnessLevel
}
function onDeviceSwitched() {
if (!brightnessSlider.pressed)
brightnessSlider.value = DisplayService.brightnessLevel
}
}
}
}
}
onOsdShown: {
if (DisplayService.brightnessAvailable && contentLoader.item) {
let slider = contentLoader.item.children[0].children[1]
if (slider)
slider.value = DisplayService.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()
}
}
}
}
}
}
-215
View File
@@ -1,215 +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
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)
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
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)
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
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: 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
}
}
}
-359
View File
@@ -1,359 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: displaysTab
property var variantComponents: [{
"id": "topBar",
"name": "Top Bar",
"description": "System bar with widgets and system information",
"icon": "toolbar"
}, {
"id": "dock",
"name": "Application Dock",
"description": "Bottom dock for pinned and running applications",
"icon": "dock"
}, {
"id": "notifications",
"name": "Notification Popups",
"description": "Notification toast popups",
"icon": "notifications"
}, {
"id": "wallpaper",
"name": "Wallpaper",
"description": "Desktop background images",
"icon": "wallpaper"
}, {
"id": "osd",
"name": "On-Screen Displays",
"description": "Volume, brightness, and other system OSDs",
"icon": "picture_in_picture"
}, {
"id": "toast",
"name": "Toast Messages",
"description": "System toast notifications",
"icon": "campaign"
}]
function getScreenPreferences(componentId) {
return SettingsData.screenPreferences && SettingsData.screenPreferences[componentId] || ["all"];
}
function setScreenPreferences(componentId, screenNames) {
var prefs = SettingsData.screenPreferences || {
};
prefs[componentId] = screenNames;
SettingsData.setScreenPreferences(prefs);
}
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
StyledRect {
width: parent.width
height: screensInfoSection.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: screensInfoSection
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
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Connected Displays"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Configure which displays show shell components"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: "Available Screens (" + Quickshell.screens.length + ")"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Repeater {
model: Quickshell.screens
delegate: Rectangle {
width: parent.width
height: screenRow.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1
Row {
id: screenRow
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: Theme.spacingM
DankIcon {
name: "desktop_windows"
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS / 2
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Row {
spacing: Theme.spacingS
StyledText {
text: modelData.width + "×" + modelData.height
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: modelData.model || "Unknown Model"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
}
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingL
Repeater {
model: displaysTab.variantComponents
delegate: StyledRect {
width: parent.width
height: componentSection.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: componentSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: modelData.icon
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: modelData.description
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: "Show on screens:"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
Column {
property string componentId: modelData.id
property var selectedScreens: displaysTab.getScreenPreferences(componentId)
width: parent.width
spacing: Theme.spacingXS
DankToggle {
width: parent.width
text: "All displays"
description: "Show on all connected displays"
checked: parent.selectedScreens.includes("all")
onToggled: (checked) => {
if (checked)
displaysTab.setScreenPreferences(parent.componentId, ["all"]);
else
displaysTab.setScreenPreferences(parent.componentId, []);
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.2
visible: !parent.selectedScreens.includes("all")
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: !parent.selectedScreens.includes("all")
Repeater {
model: Quickshell.screens
delegate: DankToggle {
property string screenName: modelData.name
property string componentId: parent.parent.componentId
width: parent.width
text: screenName
description: modelData.width + "×" + modelData.height + " • " + (modelData.model || "Unknown Model")
checked: {
var prefs = displaysTab.getScreenPreferences(componentId);
return !prefs.includes("all") && prefs.includes(screenName);
}
onToggled: (checked) => {
var currentPrefs = displaysTab.getScreenPreferences(componentId);
if (currentPrefs.includes("all"))
currentPrefs = [];
var newPrefs = currentPrefs.slice();
if (checked) {
if (!newPrefs.includes(screenName))
newPrefs.push(screenName);
} else {
var index = newPrefs.indexOf(screenName);
if (index > -1)
newPrefs.splice(index, 1);
}
displaysTab.setScreenPreferences(componentId, newPrefs);
}
}
}
}
}
}
}
}
}
}
}
}
}
-224
View File
@@ -1,224 +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
wheelEnabled: false
onSliderValueChanged: newValue => {
SettingsData.setDockTransparency(
newValue / 100)
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}
File diff suppressed because it is too large Load Diff
-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
}
}
}
}

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