mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-06 05:25:41 -05:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53fb927e36 | ||
|
|
fb5aa0313e | ||
|
|
9b41eecbf1 | ||
|
|
ae461b1caf | ||
|
|
57e36d6710 | ||
|
|
a7c4f09c5b | ||
|
|
554ef16e49 | ||
|
|
082321f860 | ||
|
|
df4f7b8c9e | ||
|
|
3f1742f074 | ||
|
|
4560d5c2d5 | ||
|
|
0ca12d275c | ||
|
|
df9e834309 | ||
|
|
ab1c0bb129 | ||
|
|
5070e4c950 | ||
|
|
c13526ccad | ||
|
|
46e16a6c69 | ||
|
|
53983933dc | ||
|
|
09f3ca39a1 | ||
|
|
42b4c91f35 |
167
CLAUDE.md
167
CLAUDE.md
@@ -63,6 +63,9 @@ quickshell -p shell.qml
|
||||
# Or use the shorthand
|
||||
qs -p .
|
||||
|
||||
# Run with verbose output for debugging
|
||||
qs -v -p shell.qml
|
||||
|
||||
# Code formatting and linting
|
||||
qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format a QML file (requires qmlfmt, do not use qmlformat)
|
||||
qmllint **/*.qml # Lint all QML files for syntax errors
|
||||
@@ -89,6 +92,7 @@ shell.qml # Main entry point (minimal orchestration)
|
||||
│ ├── DisplayService.qml
|
||||
│ ├── NotificationService.qml
|
||||
│ ├── WeatherService.qml
|
||||
│ ├── PluginService.qml
|
||||
│ └── [14 more services]
|
||||
├── Modules/ # UI components (93 files)
|
||||
│ ├── TopBar/ # Panel components (13 files)
|
||||
@@ -104,15 +108,21 @@ shell.qml # Main entry point (minimal orchestration)
|
||||
│ ├── SettingsModal.qml
|
||||
│ ├── ClipboardHistoryModal.qml
|
||||
│ ├── ProcessListModal.qml
|
||||
│ ├── PluginSettingsModal.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]
|
||||
├── Widgets/ # Reusable UI controls (19 files)
|
||||
│ ├── DankIcon.qml
|
||||
│ ├── DankSlider.qml
|
||||
│ ├── DankToggle.qml
|
||||
│ ├── DankTabBar.qml
|
||||
│ ├── DankGridView.qml
|
||||
│ ├── DankListView.qml
|
||||
│ └── [13 more widgets]
|
||||
└── plugins/ # External plugins directory ($CONFIGPATH/DankMaterialShell/plugins/)
|
||||
└── PluginName/ # Example Plugin structure
|
||||
├── plugin.json # Plugin manifest
|
||||
├── PluginNameWidget.qml # Widget component
|
||||
└── PluginNameSettings.qml # Settings UI
|
||||
```
|
||||
|
||||
### Component Organization
|
||||
@@ -163,6 +173,12 @@ shell.qml # Main entry point (minimal orchestration)
|
||||
- **DankLocationSearch**: Location picker with search
|
||||
- **SystemLogo**: Animated system branding component
|
||||
|
||||
7. **Plugins/** - External plugin system (`$CONFIGPATH/DankMaterialShell/plugins/`)
|
||||
- **PluginService**: Discovers, loads, and manages plugin lifecycle
|
||||
- **Dynamic Loading**: Plugins loaded at runtime from external directory
|
||||
- **DankBar Integration**: Plugin widgets rendered alongside built-in widgets
|
||||
- **Settings System**: Per-plugin settings with persistence
|
||||
|
||||
### Key Architectural Patterns
|
||||
|
||||
1. **Singleton Services Pattern**:
|
||||
@@ -408,10 +424,10 @@ When modifying the shell:
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
|
||||
property bool featureAvailable: false
|
||||
property type currentValue: defaultValue
|
||||
|
||||
|
||||
function performAction(param) {
|
||||
// Implementation
|
||||
}
|
||||
@@ -422,7 +438,7 @@ When modifying the shell:
|
||||
```qml
|
||||
// In module files
|
||||
property alias serviceValue: NewService.currentValue
|
||||
|
||||
|
||||
SomeControl {
|
||||
visible: NewService.featureAvailable
|
||||
enabled: NewService.featureAvailable
|
||||
@@ -430,6 +446,134 @@ When modifying the shell:
|
||||
}
|
||||
```
|
||||
|
||||
### Creating Plugins
|
||||
|
||||
Plugins are external, dynamically-loaded components that extend DankBar functionality. Plugins are stored in `~/.config/DankMaterialShell/plugins/` and have their settings isolated from core DMS settings.
|
||||
|
||||
1. **Create plugin directory**:
|
||||
```bash
|
||||
mkdir -p ~/.config/DankMaterialShell/plugins/YourPlugin
|
||||
```
|
||||
|
||||
2. **Create manifest** (`plugin.json`):
|
||||
```json
|
||||
{
|
||||
"id": "yourPlugin",
|
||||
"name": "Your Plugin",
|
||||
"description": "Widget description",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"icon": "extension",
|
||||
"component": "./YourWidget.qml",
|
||||
"settings": "./YourSettings.qml",
|
||||
"permissions": ["settings_read", "settings_write"]
|
||||
}
|
||||
```
|
||||
|
||||
3. **Create widget component** (`YourWidget.qml`):
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Services
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool compactMode: false
|
||||
property string section: "center"
|
||||
property real widgetHeight: 30
|
||||
property var pluginService: null
|
||||
|
||||
width: content.implicitWidth + 16
|
||||
height: widgetHeight
|
||||
radius: 8
|
||||
color: "#20FFFFFF"
|
||||
|
||||
Component.onCompleted: {
|
||||
if (pluginService) {
|
||||
var data = pluginService.loadPluginData("yourPlugin", "key", defaultValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Create settings component** (`YourSettings.qml`):
|
||||
```qml
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
property var pluginService: null
|
||||
|
||||
implicitHeight: settingsColumn.implicitHeight
|
||||
height: implicitHeight
|
||||
|
||||
Column {
|
||||
id: settingsColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "Your Plugin Settings"
|
||||
font.pixelSize: 18
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
// Your settings UI here
|
||||
}
|
||||
|
||||
function saveSettings(key, value) {
|
||||
if (pluginService) {
|
||||
pluginService.savePluginData("yourPlugin", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
function loadSettings(key, defaultValue) {
|
||||
if (pluginService) {
|
||||
return pluginService.loadPluginData("yourPlugin", key, defaultValue)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
5. **Enable plugin**:
|
||||
- Open Settings → Plugins
|
||||
- Click "Scan for Plugins"
|
||||
- Toggle plugin to enable
|
||||
- Add plugin ID to DankBar widget list
|
||||
|
||||
**Plugin Directory Structure:**
|
||||
```
|
||||
~/.config/DankMaterialShell/
|
||||
├── settings.json # Core DMS settings + plugin settings
|
||||
│ └── pluginSettings: {
|
||||
│ └── yourPlugin: {
|
||||
│ ├── enabled: true,
|
||||
│ └── customData: {...}
|
||||
│ }
|
||||
│ }
|
||||
└── plugins/ # Plugin files directory
|
||||
└── YourPlugin/ # Plugin directory (matches manifest ID)
|
||||
├── plugin.json # Plugin manifest
|
||||
├── YourWidget.qml # Widget component
|
||||
└── YourSettings.qml # Settings UI (optional)
|
||||
```
|
||||
|
||||
**Key Plugin APIs:**
|
||||
- `pluginService.loadPluginData(pluginId, key, default)` - Load persistent data
|
||||
- `pluginService.savePluginData(pluginId, key, value)` - Save persistent data
|
||||
- `PluginService.enablePlugin(pluginId)` - Load plugin
|
||||
- `PluginService.disablePlugin(pluginId)` - Unload plugin
|
||||
|
||||
**Important Notes:**
|
||||
- Plugin settings are automatically injected by the PluginService via `item.pluginService = PluginService`
|
||||
- Settings are stored in the main settings.json but namespaced under `pluginSettings.{pluginId}`
|
||||
- Plugin directories must match the plugin ID in the manifest
|
||||
- Use the injected `pluginService` property in both widget and settings components
|
||||
|
||||
### Debugging Common Issues
|
||||
|
||||
1. **Import errors**: Check import paths
|
||||
@@ -454,6 +598,7 @@ When modifying the shell:
|
||||
- **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
|
||||
- **Plugin System**: For user extensions, create plugins instead of modifying core modules - see docs/PLUGINS.md
|
||||
|
||||
### Common Widget Patterns
|
||||
|
||||
|
||||
53
Common/Facts.qml
Normal file
53
Common/Facts.qml
Normal file
@@ -0,0 +1,53 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var facts: [
|
||||
"A photon takes 100,000 to 200,000 years bouncing through the Sun's dense core, then races to Earth in just 8 minutes 20 seconds.",
|
||||
"A teaspoon of neutron star matter would weigh a billion metric tons here on Earth.",
|
||||
"Right now, 100 trillion solar neutrinos are passing through your body every second.",
|
||||
"The Sun converts 4 million metric tons of matter into pure energy every second—enough to power Earth for 500,000 years.",
|
||||
"The universe still glows with leftover heat from the Big Bang—just 2.7 degrees above absolute zero.",
|
||||
"There's a nebula out there that's actually colder than empty space itself.",
|
||||
"We've detected black holes crashing together by measuring spacetime stretch by less than 1/10,000th the width of a proton.",
|
||||
"Fast radio bursts can release more energy in 5 milliseconds than our Sun produces in 3 days.",
|
||||
"Our galaxy might be crawling with billions of rogue planets drifting alone in the dark.",
|
||||
"Distant galaxies can move away from us faster than light because space itself is stretching.",
|
||||
"The edge of what we can see is 46.5 billion light-years away, even though the universe is only 13.8 billion years old.",
|
||||
"The universe is mostly invisible: 5% regular matter, 27% dark matter, 68% dark energy.",
|
||||
"A day on Venus lasts longer than its entire year around the Sun.",
|
||||
"On Mercury, the time between sunrises is 176 Earth days long.",
|
||||
"In about 4.5 billion years, our galaxy will smash into Andromeda.",
|
||||
"Most of the gold in your jewelry was forged when neutron stars collided somewhere in space.",
|
||||
"PSR J1748-2446ad, the fastest spinning star, rotates 716 times per second—its equator moves at 24% the speed of light.",
|
||||
"Cosmic rays create particles that shouldn't make it to Earth's surface, but time dilation lets them sneak through.",
|
||||
"Jupiter's magnetic field is so huge that if we could see it, it would look bigger than the Moon in our sky.",
|
||||
"Interstellar space is so empty it's like a cube 32 kilometers wide containing just a single grain of sand.",
|
||||
"Voyager 1 is 24 billion kilometers away but won't leave the Sun's gravitational influence for another 30,000 years.",
|
||||
"Counting to a billion at one number per second would take over 31 years.",
|
||||
"Space is so vast, even speeding at light-speed, you'd never return past the cosmic horizon.",
|
||||
"Astronauts on the ISS age about 0.01 seconds less each year than people on Earth.",
|
||||
"Sagittarius B2, a dust cloud near our galaxy's center, contains ethyl formate—the compound that gives raspberries their flavor and rum its smell.",
|
||||
"Beyond 16 billion light-years, the cosmic event horizon marks where space expands too fast for light to ever reach us again.",
|
||||
"Even at light-speed, you'd never catch up to most galaxies—space expands faster.",
|
||||
"Only around 5% of galaxies are ever reachable—even at light-speed.",
|
||||
"If the Sun vanished, we'd still orbit it for 8 minutes before drifting away.",
|
||||
"If a planet 65 million light-years away looked at Earth now, it'd see dinosaurs.",
|
||||
"Our oldest radio signals will reach the Milky Way's center in 26,000 years.",
|
||||
"Every atom in your body heavier than hydrogen was forged in the nuclear furnace of a dying star.",
|
||||
"The Moon moves 3.8 centimeters farther from Earth every year.",
|
||||
"The universe creates 275 million new stars every single day.",
|
||||
"Jupiter's Great Red Spot is a storm twice the size of Earth that has been raging for at least 350 years.",
|
||||
"If you watched someone fall into a black hole, they'd appear frozen at the event horizon forever—time effectively stops from your perspective.",
|
||||
"The Boötes Supervoid is a cosmic desert 1.8 billion light-years across with 60% fewer galaxies than it should have."
|
||||
]
|
||||
|
||||
function getRandomFact() {
|
||||
return facts[Math.floor(Math.random() * facts.length)]
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ Singleton {
|
||||
|
||||
id: root
|
||||
|
||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
|
||||
property bool isLightMode: false
|
||||
property string wallpaperPath: ""
|
||||
property string wallpaperLastPath: ""
|
||||
@@ -71,11 +73,17 @@ Singleton {
|
||||
|
||||
|
||||
Component.onCompleted: {
|
||||
loadSettings()
|
||||
if (!isGreeterMode) {
|
||||
loadSettings()
|
||||
}
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
parseSettings(settingsFile.text())
|
||||
if (isGreeterMode) {
|
||||
parseSettings(greeterSessionFile.text())
|
||||
} else {
|
||||
parseSettings(settingsFile.text())
|
||||
}
|
||||
}
|
||||
|
||||
function parseSettings(content) {
|
||||
@@ -142,10 +150,11 @@ Singleton {
|
||||
batterySuspendTimeout = settings.batterySuspendTimeout !== undefined ? settings.batterySuspendTimeout : 0
|
||||
batteryHibernateTimeout = settings.batteryHibernateTimeout !== undefined ? settings.batteryHibernateTimeout : 0
|
||||
lockBeforeSuspend = settings.lockBeforeSuspend !== undefined ? settings.lockBeforeSuspend : false
|
||||
|
||||
// Generate system themes but don't override user's theme choice
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
|
||||
if (!isGreeterMode) {
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -154,6 +163,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
if (isGreeterMode) return
|
||||
settingsFile.setText(JSON.stringify({
|
||||
"isLightMode": isLightMode,
|
||||
"wallpaperPath": wallpaperPath,
|
||||
@@ -620,22 +630,43 @@ Singleton {
|
||||
FileView {
|
||||
id: settingsFile
|
||||
|
||||
path: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
|
||||
blockLoading: true
|
||||
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
|
||||
blockLoading: isGreeterMode
|
||||
blockWrites: true
|
||||
watchChanges: true
|
||||
watchChanges: !isGreeterMode
|
||||
onLoaded: {
|
||||
parseSettings(settingsFile.text())
|
||||
hasTriedDefaultSession = false
|
||||
if (!isGreeterMode) {
|
||||
parseSettings(settingsFile.text())
|
||||
hasTriedDefaultSession = false
|
||||
}
|
||||
}
|
||||
onLoadFailed: error => {
|
||||
if (!hasTriedDefaultSession) {
|
||||
hasTriedDefaultSession = true
|
||||
if (!isGreeterMode && !hasTriedDefaultSettings) {
|
||||
hasTriedDefaultSettings = true
|
||||
defaultSessionCheckProcess.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: greeterSessionFile
|
||||
|
||||
path: {
|
||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
|
||||
return greetCfgDir + "/session.json"
|
||||
}
|
||||
preload: isGreeterMode
|
||||
blockLoading: false
|
||||
blockWrites: true
|
||||
watchChanges: false
|
||||
printErrors: true
|
||||
onLoaded: {
|
||||
if (isGreeterMode) {
|
||||
parseSettings(greeterSessionFile.text())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: defaultSessionCheckProcess
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import qs.Services
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
|
||||
enum Position {
|
||||
Top,
|
||||
Bottom,
|
||||
@@ -173,6 +175,8 @@ Singleton {
|
||||
|
||||
property bool _loading: false
|
||||
|
||||
property var pluginSettings: ({})
|
||||
|
||||
function getEffectiveTimeFormat() {
|
||||
if (use24HourClock) {
|
||||
return Locale.ShortFormat
|
||||
@@ -359,6 +363,7 @@ Singleton {
|
||||
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch"
|
||||
surfaceBase = settings.surfaceBase !== undefined ? settings.surfaceBase : "s"
|
||||
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({})
|
||||
pluginSettings = settings.pluginSettings !== undefined ? settings.pluginSettings : ({})
|
||||
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : SettingsData.AnimationSpeed.Short
|
||||
applyStoredTheme()
|
||||
detectAvailableIconThemes()
|
||||
@@ -479,6 +484,7 @@ Singleton {
|
||||
"notificationTimeoutCritical": notificationTimeoutCritical,
|
||||
"notificationPopupPosition": notificationPopupPosition,
|
||||
"screenPreferences": screenPreferences,
|
||||
"pluginSettings": pluginSettings,
|
||||
"animationSpeed": animationSpeed
|
||||
}, null, 2))
|
||||
}
|
||||
@@ -991,13 +997,23 @@ Singleton {
|
||||
|
||||
function setShowDock(enabled) {
|
||||
showDock = enabled
|
||||
if (enabled && dankBarPosition === SettingsData.Position.Top) {
|
||||
setDockPosition(SettingsData.Position.Bottom)
|
||||
return
|
||||
}
|
||||
if (enabled && dankBarPosition === SettingsData.Position.Top) {
|
||||
setDockPosition(SettingsData.Position.Bottom)
|
||||
return
|
||||
if (enabled && dockPosition === dankBarPosition) {
|
||||
if (dankBarPosition === SettingsData.Position.Top) {
|
||||
setDockPosition(SettingsData.Position.Bottom)
|
||||
return
|
||||
}
|
||||
if (dankBarPosition === SettingsData.Position.Bottom) {
|
||||
setDockPosition(SettingsData.Position.Top)
|
||||
return
|
||||
}
|
||||
if (dankBarPosition === SettingsData.Position.Left) {
|
||||
setDockPosition(SettingsData.Position.Right)
|
||||
return
|
||||
}
|
||||
if (dankBarPosition === SettingsData.Position.Right) {
|
||||
setDockPosition(SettingsData.Position.Left)
|
||||
return
|
||||
}
|
||||
}
|
||||
saveSettings()
|
||||
}
|
||||
@@ -1144,14 +1160,22 @@ Singleton {
|
||||
|
||||
function setDankBarPosition(position) {
|
||||
dankBarPosition = position
|
||||
if (position === SettingsData.Position.Bottom && showDock) {
|
||||
if (position === SettingsData.Position.Bottom && dockPosition === SettingsData.Position.Bottom && showDock) {
|
||||
setDockPosition(SettingsData.Position.Top)
|
||||
return
|
||||
}
|
||||
if (position === SettingsData.Position.Top && showDock) {
|
||||
if (position === SettingsData.Position.Top && dockPosition === SettingsData.Position.Top && showDock) {
|
||||
setDockPosition(SettingsData.Position.Bottom)
|
||||
return
|
||||
}
|
||||
if (position === SettingsData.Position.Left && dockPosition === SettingsData.Position.Left && showDock) {
|
||||
setDockPosition(SettingsData.Position.Right)
|
||||
return
|
||||
}
|
||||
if (position === SettingsData.Position.Right && dockPosition === SettingsData.Position.Right && showDock) {
|
||||
setDockPosition(SettingsData.Position.Left)
|
||||
return
|
||||
}
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
@@ -1163,6 +1187,12 @@ Singleton {
|
||||
if (position === SettingsData.Position.Top && dankBarPosition === SettingsData.Position.Top && showDock) {
|
||||
setDankBarPosition(SettingsData.Position.Bottom)
|
||||
}
|
||||
if (position === SettingsData.Position.Left && dankBarPosition === SettingsData.Position.Left && showDock) {
|
||||
setDankBarPosition(SettingsData.Position.Right)
|
||||
}
|
||||
if (position === SettingsData.Position.Right && dankBarPosition === SettingsData.Position.Right && showDock) {
|
||||
setDankBarPosition(SettingsData.Position.Left)
|
||||
}
|
||||
saveSettings()
|
||||
Qt.callLater(() => forceDockLayoutRefresh())
|
||||
}
|
||||
@@ -1240,6 +1270,33 @@ Singleton {
|
||||
return Quickshell.screens.filter(screen => prefs.includes(screen.name))
|
||||
}
|
||||
|
||||
// Plugin settings functions
|
||||
function getPluginSetting(pluginId, key, defaultValue) {
|
||||
if (!pluginSettings[pluginId]) {
|
||||
return defaultValue
|
||||
}
|
||||
return pluginSettings[pluginId][key] !== undefined ? pluginSettings[pluginId][key] : defaultValue
|
||||
}
|
||||
|
||||
function setPluginSetting(pluginId, key, value) {
|
||||
if (!pluginSettings[pluginId]) {
|
||||
pluginSettings[pluginId] = {}
|
||||
}
|
||||
pluginSettings[pluginId][key] = value
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function removePluginSettings(pluginId) {
|
||||
if (pluginSettings[pluginId]) {
|
||||
delete pluginSettings[pluginId]
|
||||
saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
function getPluginSettingsForPlugin(pluginId) {
|
||||
return pluginSettings[pluginId] || {}
|
||||
}
|
||||
|
||||
function setAnimationSpeed(speed) {
|
||||
animationSpeed = speed
|
||||
saveSettings()
|
||||
@@ -1250,9 +1307,11 @@ Singleton {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
loadSettings()
|
||||
fontCheckTimer.start()
|
||||
initializeListModels()
|
||||
if (!isGreeterMode) {
|
||||
loadSettings()
|
||||
fontCheckTimer.start()
|
||||
initializeListModels()
|
||||
}
|
||||
}
|
||||
|
||||
ListModel {
|
||||
@@ -1293,20 +1352,22 @@ Singleton {
|
||||
FileView {
|
||||
id: settingsFile
|
||||
|
||||
path: StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
|
||||
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
|
||||
blockLoading: true
|
||||
blockWrites: true
|
||||
atomicWrites: true
|
||||
watchChanges: true
|
||||
watchChanges: !isGreeterMode
|
||||
onLoaded: {
|
||||
parseSettings(settingsFile.text())
|
||||
hasTriedDefaultSettings = false
|
||||
if (!isGreeterMode) {
|
||||
parseSettings(settingsFile.text())
|
||||
hasTriedDefaultSettings = false
|
||||
}
|
||||
}
|
||||
onLoadFailed: error => {
|
||||
if (!hasTriedDefaultSettings) {
|
||||
if (!isGreeterMode && !hasTriedDefaultSettings) {
|
||||
hasTriedDefaultSettings = true
|
||||
defaultSettingsCheckProcess.running = true
|
||||
} else {
|
||||
} else if (!isGreeterMode) {
|
||||
applyStoredTheme()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,8 @@ Singleton {
|
||||
readonly property string shellDir: Paths.strip(Qt.resolvedUrl(".").toString()).replace("/Common/", "")
|
||||
readonly property string wallpaperPath: {
|
||||
if (typeof SessionData === "undefined") return ""
|
||||
|
||||
|
||||
if (SessionData.perMonitorWallpaper) {
|
||||
// Use first monitor's wallpaper for dynamic theming
|
||||
var screens = Quickshell.screens
|
||||
if (screens.length > 0) {
|
||||
var firstMonitorWallpaper = SessionData.getMonitorWallpaper(screens[0].name)
|
||||
@@ -93,6 +92,13 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function applyGreeterTheme(themeName) {
|
||||
switchTheme(themeName, false, false)
|
||||
if (themeName === dynamic && dynamicColorsFileView.path) {
|
||||
dynamicColorsFileView.reload()
|
||||
}
|
||||
}
|
||||
|
||||
function getMatugenColor(path, fallback) {
|
||||
colorUpdateTrigger
|
||||
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark"
|
||||
@@ -303,10 +309,13 @@ Singleton {
|
||||
currentThemeCategory = "generic"
|
||||
}
|
||||
}
|
||||
if (savePrefs && typeof SettingsData !== "undefined")
|
||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
|
||||
if (savePrefs && typeof SettingsData !== "undefined" && !isGreeterMode)
|
||||
SettingsData.setTheme(currentTheme)
|
||||
|
||||
generateSystemThemesFromCurrentTheme()
|
||||
if (!isGreeterMode) {
|
||||
generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function setLightMode(light, savePrefs = true, enableTransition = false) {
|
||||
@@ -318,11 +327,14 @@ Singleton {
|
||||
return
|
||||
}
|
||||
|
||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
|
||||
isLightMode = light
|
||||
if (savePrefs && typeof SessionData !== "undefined")
|
||||
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode)
|
||||
SessionData.setLightMode(isLightMode)
|
||||
PortalService.setLightMode(isLightMode)
|
||||
generateSystemThemesFromCurrentTheme()
|
||||
if (!isGreeterMode) {
|
||||
PortalService.setLightMode(isLightMode)
|
||||
generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function toggleLightMode(savePrefs = true) {
|
||||
@@ -599,7 +611,8 @@ Singleton {
|
||||
}
|
||||
|
||||
function generateSystemThemesFromCurrentTheme() {
|
||||
if (!matugenAvailable)
|
||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
|
||||
if (!matugenAvailable || isGreeterMode)
|
||||
return
|
||||
|
||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode)
|
||||
@@ -670,8 +683,9 @@ Singleton {
|
||||
command: ["which", "matugen"]
|
||||
onExited: code => {
|
||||
matugenAvailable = (code === 0) && !envDisableMatugen
|
||||
if (!matugenAvailable) {
|
||||
console.log("matugen not not available in path or disabled via DMS_DISABLE_MATUGEN")
|
||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
|
||||
|
||||
if (!matugenAvailable || isGreeterMode) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -722,10 +736,7 @@ Singleton {
|
||||
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 (exitCode !== 0 && exitCode !== 2) {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showError("Theme worker failed (" + exitCode + ")")
|
||||
}
|
||||
@@ -814,8 +825,14 @@ Singleton {
|
||||
|
||||
FileView {
|
||||
id: dynamicColorsFileView
|
||||
path: stateDir + "/dms-colors.json"
|
||||
watchChanges: currentTheme === dynamic
|
||||
path: {
|
||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
|
||||
const colorsPath = SessionData.isGreeterMode
|
||||
? greetCfgDir + "/colors.json"
|
||||
: stateDir + "/dms-colors.json"
|
||||
return colorsPath
|
||||
}
|
||||
watchChanges: currentTheme === dynamic && !SessionData.isGreeterMode
|
||||
|
||||
function parseAndLoadColors() {
|
||||
try {
|
||||
@@ -828,6 +845,7 @@ Singleton {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Theme: Failed to parse dynamic colors:", e)
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.wallpaperErrorStatus = "error"
|
||||
ToastService.showError("Dynamic colors parse error: " + e.message)
|
||||
|
||||
25
DMSGreeter.qml
Normal file
25
DMSGreeter.qml
Normal file
@@ -0,0 +1,25 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Greetd
|
||||
import qs.Common
|
||||
import qs.Modules.Greetd
|
||||
|
||||
ShellRoot {
|
||||
id: root
|
||||
|
||||
WlSessionLock {
|
||||
id: sessionLock
|
||||
locked: true
|
||||
|
||||
onLockedChanged: {
|
||||
if (!locked) {
|
||||
console.log("Greetd session unlocked, exiting")
|
||||
}
|
||||
}
|
||||
|
||||
GreeterSurface {
|
||||
lock: sessionLock
|
||||
}
|
||||
}
|
||||
}
|
||||
641
DMSShell.qml
Normal file
641
DMSShell.qml
Normal file
@@ -0,0 +1,641 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modals
|
||||
import qs.Modals.Clipboard
|
||||
import qs.Modals.Common
|
||||
import qs.Modals.Settings
|
||||
import qs.Modals.Spotlight
|
||||
import qs.Modules
|
||||
import qs.Modules.AppDrawer
|
||||
import qs.Modules.DankDash
|
||||
import qs.Modules.ControlCenter
|
||||
import qs.Modules.Dock
|
||||
import qs.Modules.Lock
|
||||
import qs.Modules.Notepad
|
||||
import qs.Modules.Notifications.Center
|
||||
import qs.Widgets
|
||||
import qs.Modules.Notifications.Popup
|
||||
import qs.Modules.OSD
|
||||
import qs.Modules.ProcessList
|
||||
import qs.Modules.Settings
|
||||
import qs.Modules.DankBar
|
||||
import qs.Modules.DankBar.Popouts
|
||||
import qs.Modules.Plugins
|
||||
import qs.Services
|
||||
|
||||
|
||||
Item {
|
||||
Component.onCompleted: {
|
||||
PortalService.init()
|
||||
// Initialize DisplayService night mode functionality
|
||||
DisplayService.nightModeEnabled
|
||||
// Initialize WallpaperCyclingService
|
||||
WallpaperCyclingService.cyclingActive
|
||||
// Initialize PluginService by accessing its properties
|
||||
PluginService.pluginDirectory
|
||||
}
|
||||
|
||||
WallpaperBackground {}
|
||||
|
||||
Lock {
|
||||
id: lock
|
||||
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: dankBarLoader
|
||||
asynchronous: false
|
||||
|
||||
property var currentPosition: SettingsData.dankBarPosition
|
||||
|
||||
sourceComponent: DankBar {
|
||||
onColorPickerRequested: colorPickerModal.show()
|
||||
}
|
||||
|
||||
onCurrentPositionChanged: {
|
||||
const component = sourceComponent
|
||||
sourceComponent = null
|
||||
Qt.callLater(() => {
|
||||
sourceComponent = component
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: dockLoader
|
||||
active: true
|
||||
asynchronous: false
|
||||
|
||||
property var currentPosition: SettingsData.dockPosition
|
||||
|
||||
sourceComponent: Dock {
|
||||
contextMenu: dockContextMenuLoader.item ? dockContextMenuLoader.item : null
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
dockContextMenuLoader.active = true
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentPositionChanged: {
|
||||
console.log("DEBUG: Dock position changed to:", currentPosition, "- recreating dock")
|
||||
const comp = sourceComponent
|
||||
sourceComponent = null
|
||||
Qt.callLater(() => {
|
||||
sourceComponent = comp
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: dankDashPopoutLoader
|
||||
|
||||
active: false
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: Component {
|
||||
DankDashPopout {
|
||||
id: dankDashPopout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: dockContextMenuLoader
|
||||
|
||||
active: false
|
||||
|
||||
DockContextMenu {
|
||||
id: dockContextMenu
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: notificationCenterLoader
|
||||
|
||||
active: false
|
||||
|
||||
NotificationCenterPopout {
|
||||
id: notificationCenter
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("notifications")
|
||||
|
||||
delegate: NotificationPopupManager {
|
||||
modelData: item
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: controlCenterLoader
|
||||
|
||||
active: false
|
||||
|
||||
property var modalRef: colorPickerModal
|
||||
|
||||
ControlCenterPopout {
|
||||
id: controlCenterPopout
|
||||
colorPickerModal: controlCenterLoader.modalRef
|
||||
|
||||
onPowerActionRequested: (action, title, message) => {
|
||||
powerConfirmModalLoader.active = true
|
||||
if (powerConfirmModalLoader.item) {
|
||||
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
|
||||
powerConfirmModalLoader.item.show(title, message, function () {
|
||||
switch (action) {
|
||||
case "logout":
|
||||
SessionService.logout()
|
||||
break
|
||||
case "suspend":
|
||||
SessionService.suspend()
|
||||
break
|
||||
case "hibernate":
|
||||
SessionService.hibernate()
|
||||
break
|
||||
case "reboot":
|
||||
SessionService.reboot()
|
||||
break
|
||||
case "poweroff":
|
||||
SessionService.poweroff()
|
||||
break
|
||||
}
|
||||
}, function () {})
|
||||
}
|
||||
}
|
||||
onLockRequested: {
|
||||
lock.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: wifiPasswordModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
WifiPasswordModal {
|
||||
id: wifiPasswordModal
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: networkInfoModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
NetworkInfoModal {
|
||||
id: networkInfoModal
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: batteryPopoutLoader
|
||||
|
||||
active: false
|
||||
|
||||
BatteryPopout {
|
||||
id: batteryPopout
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: vpnPopoutLoader
|
||||
|
||||
active: false
|
||||
|
||||
VpnPopout {
|
||||
id: vpnPopout
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: powerMenuLoader
|
||||
|
||||
active: false
|
||||
|
||||
PowerMenu {
|
||||
id: powerMenu
|
||||
|
||||
onPowerActionRequested: (action, title, message) => {
|
||||
powerConfirmModalLoader.active = true
|
||||
if (powerConfirmModalLoader.item) {
|
||||
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
|
||||
powerConfirmModalLoader.item.show(title, message, function () {
|
||||
switch (action) {
|
||||
case "logout":
|
||||
SessionService.logout()
|
||||
break
|
||||
case "suspend":
|
||||
SessionService.suspend()
|
||||
break
|
||||
case "hibernate":
|
||||
SessionService.hibernate()
|
||||
break
|
||||
case "reboot":
|
||||
SessionService.reboot()
|
||||
break
|
||||
case "poweroff":
|
||||
SessionService.poweroff()
|
||||
break
|
||||
}
|
||||
}, function () {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: powerConfirmModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
ConfirmModal {
|
||||
id: powerConfirmModal
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: processListPopoutLoader
|
||||
|
||||
active: false
|
||||
|
||||
ProcessListPopout {
|
||||
id: processListPopout
|
||||
}
|
||||
}
|
||||
|
||||
SettingsModal {
|
||||
id: settingsModal
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: appDrawerLoader
|
||||
|
||||
active: false
|
||||
|
||||
AppDrawerPopout {
|
||||
id: appDrawerPopout
|
||||
}
|
||||
}
|
||||
|
||||
SpotlightModal {
|
||||
id: spotlightModal
|
||||
}
|
||||
|
||||
ClipboardHistoryModal {
|
||||
id: clipboardHistoryModalPopup
|
||||
}
|
||||
|
||||
NotificationModal {
|
||||
id: notificationModal
|
||||
}
|
||||
ColorPickerModal {
|
||||
id: colorPickerModal
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: processListModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
ProcessListModal {
|
||||
id: processListModal
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: systemUpdateLoader
|
||||
|
||||
active: false
|
||||
|
||||
SystemUpdatePopout {
|
||||
id: systemUpdatePopout
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
id: notepadSlideoutVariants
|
||||
model: SettingsData.getFilteredScreens("notepad")
|
||||
|
||||
delegate: DankSlideout {
|
||||
id: notepadSlideout
|
||||
modelData: item
|
||||
title: qsTr("Notepad")
|
||||
slideoutWidth: 480
|
||||
expandable: true
|
||||
expandedWidthValue: 960
|
||||
customTransparency: SettingsData.notepadTransparencyOverride
|
||||
|
||||
content: Component {
|
||||
Notepad {
|
||||
onHideRequested: {
|
||||
notepadSlideout.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (isVisible) {
|
||||
hide()
|
||||
} else {
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: powerMenuModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
PowerMenuModal {
|
||||
id: powerMenuModal
|
||||
|
||||
onPowerActionRequested: (action, title, message) => {
|
||||
powerConfirmModalLoader.active = true
|
||||
if (powerConfirmModalLoader.item) {
|
||||
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
|
||||
powerConfirmModalLoader.item.show(title, message, function () {
|
||||
switch (action) {
|
||||
case "logout":
|
||||
SessionService.logout()
|
||||
break
|
||||
case "suspend":
|
||||
SessionService.suspend()
|
||||
break
|
||||
case "hibernate":
|
||||
SessionService.hibernate()
|
||||
break
|
||||
case "reboot":
|
||||
SessionService.reboot()
|
||||
break
|
||||
case "poweroff":
|
||||
SessionService.poweroff()
|
||||
break
|
||||
}
|
||||
}, function () {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open() {
|
||||
powerMenuModalLoader.active = true
|
||||
if (powerMenuModalLoader.item)
|
||||
powerMenuModalLoader.item.open()
|
||||
|
||||
return "POWERMENU_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (powerMenuModalLoader.item)
|
||||
powerMenuModalLoader.item.close()
|
||||
|
||||
return "POWERMENU_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
powerMenuModalLoader.active = true
|
||||
if (powerMenuModalLoader.item)
|
||||
powerMenuModalLoader.item.toggle()
|
||||
|
||||
return "POWERMENU_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
target: "powermenu"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
processListModalLoader.active = true
|
||||
if (processListModalLoader.item)
|
||||
processListModalLoader.item.show()
|
||||
|
||||
return "PROCESSLIST_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (processListModalLoader.item)
|
||||
processListModalLoader.item.hide()
|
||||
|
||||
return "PROCESSLIST_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
processListModalLoader.active = true
|
||||
if (processListModalLoader.item)
|
||||
processListModalLoader.item.toggle()
|
||||
|
||||
return "PROCESSLIST_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
target: "processlist"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
controlCenterLoader.active = true
|
||||
if (controlCenterLoader.item) {
|
||||
controlCenterLoader.item.open()
|
||||
return "CONTROL_CENTER_OPEN_SUCCESS"
|
||||
}
|
||||
return "CONTROL_CENTER_OPEN_FAILED"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (controlCenterLoader.item) {
|
||||
controlCenterLoader.item.close()
|
||||
return "CONTROL_CENTER_CLOSE_SUCCESS"
|
||||
}
|
||||
return "CONTROL_CENTER_CLOSE_FAILED"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
controlCenterLoader.active = true
|
||||
if (controlCenterLoader.item) {
|
||||
controlCenterLoader.item.toggle()
|
||||
return "CONTROL_CENTER_TOGGLE_SUCCESS"
|
||||
}
|
||||
return "CONTROL_CENTER_TOGGLE_FAILED"
|
||||
}
|
||||
|
||||
target: "control-center"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(tab: string): string {
|
||||
dankDashPopoutLoader.active = true
|
||||
if (dankDashPopoutLoader.item) {
|
||||
switch (tab.toLowerCase()) {
|
||||
case "media":
|
||||
dankDashPopoutLoader.item.currentTabIndex = 1
|
||||
break
|
||||
case "weather":
|
||||
dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0
|
||||
break
|
||||
default:
|
||||
dankDashPopoutLoader.item.currentTabIndex = 0
|
||||
break
|
||||
}
|
||||
dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen)
|
||||
dankDashPopoutLoader.item.dashVisible = true
|
||||
return "DASH_OPEN_SUCCESS"
|
||||
}
|
||||
return "DASH_OPEN_FAILED"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (dankDashPopoutLoader.item) {
|
||||
dankDashPopoutLoader.item.dashVisible = false
|
||||
return "DASH_CLOSE_SUCCESS"
|
||||
}
|
||||
return "DASH_CLOSE_FAILED"
|
||||
}
|
||||
|
||||
function toggle(tab: string): string {
|
||||
dankDashPopoutLoader.active = true
|
||||
if (dankDashPopoutLoader.item) {
|
||||
if (dankDashPopoutLoader.item.dashVisible) {
|
||||
dankDashPopoutLoader.item.dashVisible = false
|
||||
} else {
|
||||
switch (tab.toLowerCase()) {
|
||||
case "media":
|
||||
dankDashPopoutLoader.item.currentTabIndex = 1
|
||||
break
|
||||
case "weather":
|
||||
dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0
|
||||
break
|
||||
default:
|
||||
dankDashPopoutLoader.item.currentTabIndex = 0
|
||||
break
|
||||
}
|
||||
dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen)
|
||||
dankDashPopoutLoader.item.dashVisible = true
|
||||
}
|
||||
return "DASH_TOGGLE_SUCCESS"
|
||||
}
|
||||
return "DASH_TOGGLE_FAILED"
|
||||
}
|
||||
|
||||
target: "dash"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function getFocusedScreenName() {
|
||||
if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) {
|
||||
return Hyprland.focusedWorkspace.monitor.name
|
||||
}
|
||||
if (CompositorService.isNiri && NiriService.currentOutput) {
|
||||
return NiriService.currentOutput
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function getActiveNotepadInstance() {
|
||||
if (notepadSlideoutVariants.instances.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (notepadSlideoutVariants.instances.length === 1) {
|
||||
return notepadSlideoutVariants.instances[0]
|
||||
}
|
||||
|
||||
var focusedScreen = getFocusedScreenName()
|
||||
if (focusedScreen && notepadSlideoutVariants.instances.length > 0) {
|
||||
for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) {
|
||||
var slideout = notepadSlideoutVariants.instances[i]
|
||||
if (slideout.modelData && slideout.modelData.name === focusedScreen) {
|
||||
return slideout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) {
|
||||
var slideout = notepadSlideoutVariants.instances[i]
|
||||
if (slideout.isVisible) {
|
||||
return slideout
|
||||
}
|
||||
}
|
||||
|
||||
return notepadSlideoutVariants.instances[0]
|
||||
}
|
||||
|
||||
function open(): string {
|
||||
var instance = getActiveNotepadInstance()
|
||||
if (instance) {
|
||||
instance.show()
|
||||
return "NOTEPAD_OPEN_SUCCESS"
|
||||
}
|
||||
return "NOTEPAD_OPEN_FAILED"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
var instance = getActiveNotepadInstance()
|
||||
if (instance) {
|
||||
instance.hide()
|
||||
return "NOTEPAD_CLOSE_SUCCESS"
|
||||
}
|
||||
return "NOTEPAD_CLOSE_FAILED"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
var instance = getActiveNotepadInstance()
|
||||
if (instance) {
|
||||
instance.toggle()
|
||||
return "NOTEPAD_TOGGLE_SUCCESS"
|
||||
}
|
||||
return "NOTEPAD_TOGGLE_FAILED"
|
||||
}
|
||||
|
||||
target: "notepad"
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("toast")
|
||||
|
||||
delegate: Toast {
|
||||
modelData: item
|
||||
visible: ToastService.toastVisible
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("osd")
|
||||
|
||||
delegate: VolumeOSD {
|
||||
modelData: item
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("osd")
|
||||
|
||||
delegate: MicMuteOSD {
|
||||
modelData: item
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("osd")
|
||||
|
||||
delegate: BrightnessOSD {
|
||||
modelData: item
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("osd")
|
||||
|
||||
delegate: IdleInhibitorOSD {
|
||||
modelData: item
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,6 @@ Item {
|
||||
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||
|
||||
width: parent.width
|
||||
text: "Automatically lock after"
|
||||
options: timeoutOptions
|
||||
|
||||
@@ -116,7 +115,6 @@ Item {
|
||||
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||
|
||||
width: parent.width
|
||||
text: "Turn off monitors after"
|
||||
options: timeoutOptions
|
||||
|
||||
@@ -153,7 +151,6 @@ Item {
|
||||
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||
|
||||
width: parent.width
|
||||
text: "Suspend system after"
|
||||
options: timeoutOptions
|
||||
|
||||
@@ -190,7 +187,6 @@ Item {
|
||||
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||
|
||||
width: parent.width
|
||||
text: "Hibernate system after"
|
||||
options: timeoutOptions
|
||||
visible: SessionService.hibernateSupported
|
||||
|
||||
@@ -154,13 +154,26 @@ Item {
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: aboutLoader
|
||||
id: pluginsLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 10
|
||||
visible: active
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: PluginsTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: aboutLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 11
|
||||
visible: active
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: AboutTab {
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ DankModal {
|
||||
|
||||
objectName: "settingsModal"
|
||||
width: 800
|
||||
height: 750
|
||||
height: 800
|
||||
visible: false
|
||||
onBackgroundClicked: () => {
|
||||
return hide();
|
||||
|
||||
@@ -38,6 +38,9 @@ Rectangle {
|
||||
}, {
|
||||
"text": "Power",
|
||||
"icon": "power_settings_new"
|
||||
}, {
|
||||
"text": "Plugins",
|
||||
"icon": "extension"
|
||||
}, {
|
||||
"text": "About",
|
||||
"icon": "info"
|
||||
|
||||
@@ -271,18 +271,17 @@ DankPopout {
|
||||
spacing: Theme.spacingM
|
||||
visible: searchField.text.length === 0
|
||||
leftPadding: Theme.spacingS
|
||||
topPadding: Theme.spacingXS
|
||||
|
||||
Item {
|
||||
width: 200
|
||||
height: 36
|
||||
Rectangle {
|
||||
width: 180
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
|
||||
DankDropdown {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
text: ""
|
||||
dropdownWidth: 180
|
||||
currentValue: appLauncher.selectedCategory
|
||||
options: appLauncher.categories
|
||||
optionIcons: appLauncher.categoryIcons
|
||||
@@ -293,7 +292,7 @@ DankPopout {
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 310
|
||||
width: parent.width - 290
|
||||
height: 1
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ Item {
|
||||
property var components: null
|
||||
property bool noBackground: false
|
||||
required property var axis
|
||||
property string section: "center"
|
||||
property var parentScreen: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
|
||||
readonly property bool isVertical: axis?.isVertical ?? false
|
||||
readonly property real spacing: noBackground ? 2 : Theme.spacingXS
|
||||
@@ -266,7 +270,8 @@ Item {
|
||||
}
|
||||
|
||||
function getWidgetComponent(widgetId) {
|
||||
const componentMap = {
|
||||
// Build dynamic component map including plugins
|
||||
let baseMap = {
|
||||
"launcherButton": "launcherButtonComponent",
|
||||
"workspaceSwitcher": "workspaceSwitcherComponent",
|
||||
"focusedWindow": "focusedWindowComponent",
|
||||
@@ -296,8 +301,15 @@ Item {
|
||||
"systemUpdate": "systemUpdateComponent"
|
||||
}
|
||||
|
||||
const componentKey = componentMap[widgetId]
|
||||
return componentKey ? root.components[componentKey] : null
|
||||
// For built-in components, get from components property
|
||||
const componentKey = baseMap[widgetId]
|
||||
if (componentKey && root.components[componentKey]) {
|
||||
return root.components[componentKey]
|
||||
}
|
||||
|
||||
// For plugin components, get from PluginService
|
||||
let pluginMap = PluginService.getWidgetComponents()
|
||||
return pluginMap[widgetId] || null
|
||||
}
|
||||
|
||||
height: parent.height
|
||||
@@ -337,6 +349,7 @@ Item {
|
||||
id: centerRepeater
|
||||
model: root.widgetsModel
|
||||
|
||||
|
||||
Loader {
|
||||
property string widgetId: model.widgetId
|
||||
property var widgetData: model
|
||||
@@ -362,8 +375,34 @@ Item {
|
||||
item.axis = root.axis
|
||||
}
|
||||
if (root.axis && "isVertical" in item) {
|
||||
item.isVertical = root.axis.isVertical
|
||||
try {
|
||||
item.isVertical = root.axis.isVertical
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
// Inject properties for plugin widgets
|
||||
if ("section" in item) {
|
||||
item.section = root.section
|
||||
}
|
||||
if ("parentScreen" in item) {
|
||||
item.parentScreen = root.parentScreen
|
||||
}
|
||||
if ("widgetThickness" in item) {
|
||||
item.widgetThickness = root.widgetThickness
|
||||
}
|
||||
if ("barThickness" in item) {
|
||||
item.barThickness = root.barThickness
|
||||
}
|
||||
|
||||
// Inject PluginService for plugin widgets
|
||||
if (item.pluginService !== undefined) {
|
||||
if (item.pluginId !== undefined) {
|
||||
item.pluginId = model.widgetId
|
||||
}
|
||||
item.pluginService = PluginService
|
||||
}
|
||||
|
||||
layoutTimer.restart()
|
||||
}
|
||||
|
||||
@@ -379,4 +418,27 @@ Item {
|
||||
layoutTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for plugin changes and refresh components
|
||||
Connections {
|
||||
target: PluginService
|
||||
function onPluginLoaded(pluginId) {
|
||||
// Force refresh of component lookups
|
||||
for (var i = 0; i < centerRepeater.count; i++) {
|
||||
var item = centerRepeater.itemAt(i)
|
||||
if (item && item.widgetId === pluginId) {
|
||||
item.sourceComponent = root.getWidgetComponent(pluginId)
|
||||
}
|
||||
}
|
||||
}
|
||||
function onPluginUnloaded(pluginId) {
|
||||
// Force refresh of component lookups
|
||||
for (var i = 0; i < centerRepeater.count; i++) {
|
||||
var item = centerRepeater.itemAt(i)
|
||||
if (item && item.widgetId === pluginId) {
|
||||
item.sourceComponent = root.getWidgetComponent(pluginId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,18 @@ Item {
|
||||
Qt.callLater(() => Qt.callLater(forceWidgetRefresh))
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: PluginService
|
||||
function onPluginLoaded(pluginId) {
|
||||
console.log("DankBar: Plugin loaded:", pluginId)
|
||||
SettingsData.widgetDataChanged()
|
||||
}
|
||||
function onPluginUnloaded(pluginId) {
|
||||
console.log("DankBar: Plugin unloaded:", pluginId)
|
||||
SettingsData.widgetDataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
function forceWidgetRefresh() {
|
||||
}
|
||||
|
||||
@@ -315,9 +327,10 @@ Item {
|
||||
bottom: barWindow.isVertical ? parent.bottom : undefined
|
||||
}
|
||||
// Only enable mouse handling while hidden (for reveal-on-edge logic).
|
||||
hoverEnabled: SettingsData.dankBarAutoHide && !topBarCore.reveal
|
||||
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && SettingsData.dankBarOpenOnOverview
|
||||
hoverEnabled: SettingsData.dankBarAutoHide && !topBarCore.reveal && !inOverview
|
||||
acceptedButtons: Qt.NoButton
|
||||
enabled: SettingsData.dankBarAutoHide && !topBarCore.reveal
|
||||
enabled: SettingsData.dankBarAutoHide && !topBarCore.reveal && !inOverview
|
||||
|
||||
Item {
|
||||
id: topBarContainer
|
||||
@@ -366,7 +379,13 @@ Item {
|
||||
anchors.bottomMargin: !barWindow.isVertical ? SettingsData.dankBarInnerPadding / 2 : Math.max(Theme.spacingXS, SettingsData.dankBarInnerPadding * 0.8)
|
||||
clip: true
|
||||
|
||||
readonly property int availableWidth: width
|
||||
property int componentMapRevision: 0
|
||||
|
||||
function updateComponentMap() {
|
||||
componentMapRevision++
|
||||
}
|
||||
|
||||
readonly property int availableWidth: width
|
||||
readonly property int launcherButtonWidth: 40
|
||||
readonly property int workspaceSwitcherWidth: 120
|
||||
readonly property int focusedAppMaxWidth: 456
|
||||
@@ -421,35 +440,44 @@ Item {
|
||||
return widgetVisibility[widgetId] ?? true
|
||||
}
|
||||
|
||||
readonly property var componentMap: ({
|
||||
"launcherButton": launcherButtonComponent,
|
||||
"workspaceSwitcher": workspaceSwitcherComponent,
|
||||
"focusedWindow": focusedWindowComponent,
|
||||
"runningApps": runningAppsComponent,
|
||||
"clock": clockComponent,
|
||||
"music": mediaComponent,
|
||||
"weather": weatherComponent,
|
||||
"systemTray": systemTrayComponent,
|
||||
"privacyIndicator": privacyIndicatorComponent,
|
||||
"clipboard": clipboardComponent,
|
||||
"cpuUsage": cpuUsageComponent,
|
||||
"memUsage": memUsageComponent,
|
||||
"diskUsage": diskUsageComponent,
|
||||
"cpuTemp": cpuTempComponent,
|
||||
"gpuTemp": gpuTempComponent,
|
||||
"notificationButton": notificationButtonComponent,
|
||||
"battery": batteryComponent,
|
||||
"controlCenterButton": controlCenterButtonComponent,
|
||||
"idleInhibitor": idleInhibitorComponent,
|
||||
"spacer": spacerComponent,
|
||||
"separator": separatorComponent,
|
||||
"network_speed_monitor": networkComponent,
|
||||
"keyboard_layout_name": keyboardLayoutNameComponent,
|
||||
"vpn": vpnComponent,
|
||||
"notepadButton": notepadButtonComponent,
|
||||
"colorPicker": colorPickerComponent,
|
||||
"systemUpdate": systemUpdateComponent
|
||||
})
|
||||
readonly property var componentMap: {
|
||||
// This property depends on componentMapRevision to ensure it updates when plugins change
|
||||
componentMapRevision;
|
||||
|
||||
let baseMap = {
|
||||
"launcherButton": launcherButtonComponent,
|
||||
"workspaceSwitcher": workspaceSwitcherComponent,
|
||||
"focusedWindow": focusedWindowComponent,
|
||||
"runningApps": runningAppsComponent,
|
||||
"clock": clockComponent,
|
||||
"music": mediaComponent,
|
||||
"weather": weatherComponent,
|
||||
"systemTray": systemTrayComponent,
|
||||
"privacyIndicator": privacyIndicatorComponent,
|
||||
"clipboard": clipboardComponent,
|
||||
"cpuUsage": cpuUsageComponent,
|
||||
"memUsage": memUsageComponent,
|
||||
"diskUsage": diskUsageComponent,
|
||||
"cpuTemp": cpuTempComponent,
|
||||
"gpuTemp": gpuTempComponent,
|
||||
"notificationButton": notificationButtonComponent,
|
||||
"battery": batteryComponent,
|
||||
"controlCenterButton": controlCenterButtonComponent,
|
||||
"idleInhibitor": idleInhibitorComponent,
|
||||
"spacer": spacerComponent,
|
||||
"separator": separatorComponent,
|
||||
"network_speed_monitor": networkComponent,
|
||||
"keyboard_layout_name": keyboardLayoutNameComponent,
|
||||
"vpn": vpnComponent,
|
||||
"notepadButton": notepadButtonComponent,
|
||||
"colorPicker": colorPickerComponent,
|
||||
"systemUpdate": systemUpdateComponent
|
||||
}
|
||||
|
||||
// Merge with plugin widgets
|
||||
let pluginMap = PluginService.getWidgetComponents()
|
||||
return Object.assign(baseMap, pluginMap)
|
||||
}
|
||||
|
||||
function getWidgetComponent(widgetId) {
|
||||
return componentMap[widgetId] || null
|
||||
@@ -504,6 +532,9 @@ Item {
|
||||
widgetsModel: SettingsData.dankBarLeftWidgetsModel
|
||||
components: topBarContent.allComponents
|
||||
noBackground: SettingsData.dankBarNoBackground
|
||||
parentScreen: barWindow.screen
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
}
|
||||
|
||||
RightSection {
|
||||
@@ -516,6 +547,9 @@ Item {
|
||||
widgetsModel: SettingsData.dankBarRightWidgetsModel
|
||||
components: topBarContent.allComponents
|
||||
noBackground: SettingsData.dankBarNoBackground
|
||||
parentScreen: barWindow.screen
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
}
|
||||
|
||||
CenterSection {
|
||||
@@ -528,6 +562,9 @@ Item {
|
||||
widgetsModel: SettingsData.dankBarCenterWidgetsModel
|
||||
components: topBarContent.allComponents
|
||||
noBackground: SettingsData.dankBarNoBackground
|
||||
parentScreen: barWindow.screen
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,6 +584,9 @@ Item {
|
||||
widgetsModel: SettingsData.dankBarLeftWidgetsModel
|
||||
components: topBarContent.allComponents
|
||||
noBackground: SettingsData.dankBarNoBackground
|
||||
parentScreen: barWindow.screen
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
}
|
||||
|
||||
CenterSection {
|
||||
@@ -560,6 +600,9 @@ Item {
|
||||
widgetsModel: SettingsData.dankBarCenterWidgetsModel
|
||||
components: topBarContent.allComponents
|
||||
noBackground: SettingsData.dankBarNoBackground
|
||||
parentScreen: barWindow.screen
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
}
|
||||
|
||||
RightSection {
|
||||
@@ -1016,6 +1059,7 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ Item {
|
||||
property var components: null
|
||||
property bool noBackground: false
|
||||
required property var axis
|
||||
property var parentScreen: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
|
||||
readonly property bool isVertical: axis?.isVertical ?? false
|
||||
|
||||
@@ -38,6 +41,10 @@ Item {
|
||||
components: root.components
|
||||
isInColumn: false
|
||||
axis: root.axis
|
||||
section: "left"
|
||||
parentScreen: root.parentScreen
|
||||
widgetThickness: root.widgetThickness
|
||||
barThickness: root.barThickness
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +70,10 @@ Item {
|
||||
components: root.components
|
||||
isInColumn: true
|
||||
axis: root.axis
|
||||
section: "left"
|
||||
parentScreen: root.parentScreen
|
||||
widgetThickness: root.widgetThickness
|
||||
barThickness: root.barThickness
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ DankPopout {
|
||||
height: 32
|
||||
radius: 16
|
||||
color: closeBatteryArea.containsMouse ? Theme.errorHover : "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.top: parent.top
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
|
||||
@@ -8,6 +8,9 @@ Item {
|
||||
property var components: null
|
||||
property bool noBackground: false
|
||||
required property var axis
|
||||
property var parentScreen: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
|
||||
readonly property bool isVertical: axis?.isVertical ?? false
|
||||
|
||||
@@ -40,6 +43,10 @@ Item {
|
||||
components: root.components
|
||||
isInColumn: false
|
||||
axis: root.axis
|
||||
section: "right"
|
||||
parentScreen: root.parentScreen
|
||||
widgetThickness: root.widgetThickness
|
||||
barThickness: root.barThickness
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,6 +72,10 @@ Item {
|
||||
components: root.components
|
||||
isInColumn: true
|
||||
axis: root.axis
|
||||
section: "right"
|
||||
parentScreen: root.parentScreen
|
||||
widgetThickness: root.widgetThickness
|
||||
barThickness: root.barThickness
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ Loader {
|
||||
property var components: null
|
||||
property bool isInColumn: false
|
||||
property var axis: null
|
||||
property string section: "center"
|
||||
property var parentScreen: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
|
||||
asynchronous: false
|
||||
|
||||
@@ -21,17 +25,64 @@ Loader {
|
||||
|
||||
signal contentItemReady(var item)
|
||||
|
||||
Binding {
|
||||
target: root.item
|
||||
when: root.item && "parentScreen" in root.item
|
||||
property: "parentScreen"
|
||||
value: root.parentScreen
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: root.item
|
||||
when: root.item && "section" in root.item
|
||||
property: "section"
|
||||
value: root.section
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: root.item
|
||||
when: root.item && "widgetThickness" in root.item
|
||||
property: "widgetThickness"
|
||||
value: root.widgetThickness
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: root.item
|
||||
when: root.item && "barThickness" in root.item
|
||||
property: "barThickness"
|
||||
value: root.barThickness
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: root.item
|
||||
when: root.item && "axis" in root.item
|
||||
property: "axis"
|
||||
value: root.axis
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
contentItemReady(item)
|
||||
if (widgetId === "spacer") {
|
||||
item.spacerSize = Qt.binding(() => spacerSize)
|
||||
}
|
||||
if (axis && "axis" in item) {
|
||||
item.axis = axis
|
||||
}
|
||||
if (axis && "isVertical" in item) {
|
||||
item.isVertical = axis.isVertical
|
||||
try {
|
||||
item.isVertical = axis.isVertical
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (item.pluginService !== undefined) {
|
||||
if (item.pluginId !== undefined) {
|
||||
item.pluginId = widgetId
|
||||
}
|
||||
item.pluginService = PluginService
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,7 +118,12 @@ Loader {
|
||||
"systemUpdate": components.systemUpdateComponent
|
||||
}
|
||||
|
||||
return componentMap[widgetId] || null
|
||||
if (componentMap[widgetId]) {
|
||||
return componentMap[widgetId]
|
||||
}
|
||||
|
||||
let pluginMap = PluginService.getWidgetComponents()
|
||||
return pluginMap[widgetId] || null
|
||||
}
|
||||
|
||||
function getWidgetVisible(widgetId, dgopAvailable) {
|
||||
|
||||
@@ -13,83 +13,22 @@ Card {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Item {
|
||||
DankCircularImage {
|
||||
id: avatarContainer
|
||||
|
||||
property bool hasImage: profileImageLoader.status === Image.Ready
|
||||
|
||||
|
||||
width: 77
|
||||
height: 77
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 36
|
||||
color: Theme.primary
|
||||
visible: !avatarContainer.hasImage
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: UserInfoService.username.length > 0 ? UserInfoService.username.charAt(0).toUpperCase() : "b"
|
||||
font.pixelSize: Theme.fontSizeXLarge + 4
|
||||
font.weight: Font.Bold
|
||||
color: Theme.background
|
||||
}
|
||||
}
|
||||
|
||||
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: 2
|
||||
source: profileImageLoader
|
||||
maskEnabled: true
|
||||
maskSource: circularMask
|
||||
visible: avatarContainer.hasImage
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
|
||||
Item {
|
||||
id: circularMask
|
||||
width: 77 - 4
|
||||
height: 77 - 4
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "black"
|
||||
antialiasing: true
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "person"
|
||||
size: Theme.iconSize + 8
|
||||
color: Theme.error
|
||||
visible: PortalService.profileImage !== "" && profileImageLoader.status === Image.Error
|
||||
imageSource: {
|
||||
if (PortalService.profileImage === "")
|
||||
return ""
|
||||
|
||||
if (PortalService.profileImage.startsWith("/"))
|
||||
return "file://" + PortalService.profileImage
|
||||
|
||||
return PortalService.profileImage
|
||||
}
|
||||
fallbackIcon: "person"
|
||||
}
|
||||
|
||||
Column {
|
||||
@@ -104,7 +43,7 @@ Card {
|
||||
elide: Text.ElideRight
|
||||
width: parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3
|
||||
}
|
||||
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
@@ -128,7 +67,7 @@ Card {
|
||||
width: parent.parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3 - 16 - Theme.spacingS
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
@@ -141,7 +80,7 @@ Card {
|
||||
|
||||
StyledText {
|
||||
id: uptimeText
|
||||
|
||||
|
||||
property real availableWidth: parent.parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3 - 16 - Theme.spacingS
|
||||
property real longTextWidth: {
|
||||
const fontSize = Math.round(Theme.fontSizeSmall || 12)
|
||||
|
||||
@@ -20,11 +20,13 @@ Variants {
|
||||
|
||||
WlrLayershell.namespace: "quickshell:dock"
|
||||
|
||||
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
|
||||
|
||||
anchors {
|
||||
top: SettingsData.dockPosition === SettingsData.Position.Top
|
||||
bottom: SettingsData.dockPosition === SettingsData.Position.Bottom
|
||||
left: true
|
||||
right: true
|
||||
top: !isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top) : true
|
||||
bottom: !isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom) : true
|
||||
left: !isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Left)
|
||||
right: !isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Right)
|
||||
}
|
||||
|
||||
property var modelData: item
|
||||
@@ -35,12 +37,20 @@ Variants {
|
||||
readonly property real widgetHeight: Math.max(20, 26 + SettingsData.dankBarInnerPadding * 0.6)
|
||||
readonly property real effectiveBarHeight: Math.max(widgetHeight + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding))
|
||||
readonly property real barSpacing: {
|
||||
// Only add spacing if bar is visible, horizontal (Top/Bottom), and at same position as dock
|
||||
const barIsHorizontal = (SettingsData.dankBarPosition === SettingsData.Position.Top || SettingsData.dankBarPosition === SettingsData.Position.Bottom)
|
||||
const barIsVertical = (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right)
|
||||
const samePosition = (SettingsData.dockPosition === SettingsData.dankBarPosition)
|
||||
return (SettingsData.dankBarVisible && barIsHorizontal && samePosition)
|
||||
? (SettingsData.dankBarSpacing + effectiveBarHeight + SettingsData.dankBarBottomGap)
|
||||
: 0
|
||||
const dockIsHorizontal = !isVertical
|
||||
const dockIsVertical = isVertical
|
||||
|
||||
if (!SettingsData.dankBarVisible) return 0
|
||||
if (dockIsHorizontal && barIsHorizontal && samePosition) {
|
||||
return SettingsData.dankBarSpacing + effectiveBarHeight + SettingsData.dankBarBottomGap
|
||||
}
|
||||
if (dockIsVertical && barIsVertical && samePosition) {
|
||||
return SettingsData.dankBarSpacing + effectiveBarHeight + SettingsData.dankBarBottomGap
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
readonly property real dockMargin: SettingsData.dockSpacing
|
||||
@@ -103,6 +113,83 @@ Variants {
|
||||
item: dockMouseArea
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: appTooltip
|
||||
z: 1000
|
||||
|
||||
property var hoveredButton: {
|
||||
if (!dockApps.children[0]) {
|
||||
return null
|
||||
}
|
||||
const layoutItem = dockApps.children[0]
|
||||
const flowLayout = layoutItem.children[0]
|
||||
let repeater = null
|
||||
for (var i = 0; i < flowLayout.children.length; i++) {
|
||||
const child = flowLayout.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++) {
|
||||
const 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: px(tooltipLabel.implicitWidth + 24)
|
||||
height: px(tooltipLabel.implicitHeight + 12)
|
||||
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.color: Theme.outlineMedium
|
||||
|
||||
x: {
|
||||
if (!hoveredButton) return 0
|
||||
const buttonPos = hoveredButton.mapToItem(dock.contentItem, 0, 0)
|
||||
if (!dock.isVertical) {
|
||||
return buttonPos.x + hoveredButton.width / 2 - width / 2
|
||||
} else {
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Right) {
|
||||
return buttonPos.x - width - Theme.spacingS
|
||||
} else {
|
||||
return buttonPos.x + hoveredButton.width + Theme.spacingS
|
||||
}
|
||||
}
|
||||
}
|
||||
y: {
|
||||
if (!hoveredButton) return 0
|
||||
const buttonPos = hoveredButton.mapToItem(dock.contentItem, 0, 0)
|
||||
if (!dock.isVertical) {
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
|
||||
return buttonPos.y - height - Theme.spacingS
|
||||
} else {
|
||||
return buttonPos.y + hoveredButton.height + Theme.spacingS
|
||||
}
|
||||
} else {
|
||||
return buttonPos.y + hoveredButton.height / 2 - height / 2
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: tooltipLabel
|
||||
anchors.centerIn: parent
|
||||
text: appTooltip.tooltipText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dockCore
|
||||
anchors.fill: parent
|
||||
@@ -125,14 +212,31 @@ Variants {
|
||||
id: dockMouseArea
|
||||
property real currentScreen: modelData ? modelData : dock.screen
|
||||
property real screenWidth: currentScreen ? currentScreen.geometry.width : 1920
|
||||
property real screenHeight: currentScreen ? currentScreen.geometry.height : 1080
|
||||
property real maxDockWidth: Math.min(screenWidth * 0.8, 1200)
|
||||
property real maxDockHeight: Math.min(screenHeight * 0.8, 1200)
|
||||
|
||||
height: dock.reveal ? px(58 + SettingsData.dockSpacing + SettingsData.dockBottomGap) : 1
|
||||
width: dock.reveal ? Math.min(dockBackground.implicitWidth + 32, maxDockWidth) : Math.min(Math.max(dockBackground.implicitWidth + 64, 200), screenWidth * 0.5)
|
||||
height: {
|
||||
if (dock.isVertical) {
|
||||
return dock.reveal ? Math.min(dockBackground.implicitHeight + 32, maxDockHeight) : Math.min(Math.max(dockBackground.implicitHeight + 64, 200), screenHeight * 0.5)
|
||||
} else {
|
||||
return dock.reveal ? px(58 + SettingsData.dockSpacing + SettingsData.dockBottomGap) : 1
|
||||
}
|
||||
}
|
||||
width: {
|
||||
if (dock.isVertical) {
|
||||
return dock.reveal ? px(58 + SettingsData.dockSpacing + SettingsData.dockBottomGap) : 1
|
||||
} else {
|
||||
return dock.reveal ? Math.min(dockBackground.implicitWidth + 32, maxDockWidth) : Math.min(Math.max(dockBackground.implicitWidth + 64, 200), screenWidth * 0.5)
|
||||
}
|
||||
}
|
||||
anchors {
|
||||
top: SettingsData.dockPosition === SettingsData.Position.Bottom ? undefined : parent.top
|
||||
bottom: SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? undefined : parent.top) : undefined
|
||||
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined
|
||||
horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
|
||||
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.left) : undefined
|
||||
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
|
||||
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
|
||||
}
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
@@ -144,6 +248,12 @@ Variants {
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dockContainer
|
||||
@@ -151,7 +261,17 @@ Variants {
|
||||
|
||||
transform: Translate {
|
||||
id: dockSlide
|
||||
x: {
|
||||
if (!dock.isVertical) return 0
|
||||
if (dock.reveal) return 0
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Right) {
|
||||
return 60
|
||||
} else {
|
||||
return -60
|
||||
}
|
||||
}
|
||||
y: {
|
||||
if (dock.isVertical) return 0
|
||||
if (dock.reveal) return 0
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
|
||||
return 60
|
||||
@@ -160,6 +280,13 @@ Variants {
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
@@ -172,15 +299,20 @@ Variants {
|
||||
id: dockBackground
|
||||
objectName: "dockBackground"
|
||||
anchors {
|
||||
top: SettingsData.dockPosition === SettingsData.Position.Bottom ? undefined : parent.top
|
||||
bottom: SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? undefined : parent.top) : undefined
|
||||
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined
|
||||
horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
|
||||
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.left) : undefined
|
||||
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
|
||||
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
|
||||
}
|
||||
anchors.topMargin: SettingsData.dockPosition === SettingsData.Position.Bottom ? 0 : barSpacing + 4
|
||||
anchors.bottomMargin: SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + 1 : 0
|
||||
anchors.topMargin: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? 0 : barSpacing + 4) : 0
|
||||
anchors.bottomMargin: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + 1 : 0) : 0
|
||||
anchors.leftMargin: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? 0 : barSpacing + 4) : 0
|
||||
anchors.rightMargin: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + 1 : 0) : 0
|
||||
|
||||
implicitWidth: dockApps.implicitWidth + SettingsData.dockSpacing * 2
|
||||
implicitHeight: dockApps.implicitHeight + SettingsData.dockSpacing * 2
|
||||
implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2)
|
||||
implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2)
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
@@ -199,69 +331,24 @@ Variants {
|
||||
DockApps {
|
||||
id: dockApps
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: SettingsData.dockSpacing
|
||||
anchors.bottomMargin: SettingsData.dockSpacing
|
||||
anchors.top: !dock.isVertical ? parent.top : undefined
|
||||
anchors.bottom: !dock.isVertical ? parent.bottom : undefined
|
||||
anchors.horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
|
||||
anchors.left: dock.isVertical ? parent.left : undefined
|
||||
anchors.right: dock.isVertical ? parent.right : undefined
|
||||
anchors.verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
|
||||
anchors.topMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
anchors.bottomMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
anchors.leftMargin: dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
|
||||
contextMenu: dockVariants.contextMenu
|
||||
groupByApp: dock.groupByApp
|
||||
isVertical: dock.isVertical
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: appTooltip
|
||||
|
||||
property var hoveredButton: {
|
||||
if (!dockApps.children[0]) {
|
||||
return null
|
||||
}
|
||||
const row = dockApps.children[0]
|
||||
let repeater = null
|
||||
for (var i = 0; i < row.children.length; i++) {
|
||||
const 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++) {
|
||||
const 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: px(tooltipLabel.implicitWidth + 24)
|
||||
height: px(tooltipLabel.implicitHeight + 12)
|
||||
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.color: Theme.outlineMedium
|
||||
|
||||
y: SettingsData.dockPosition === SettingsData.Position.Bottom ? -height - Theme.spacingS : parent.height + Theme.spacingS
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,10 @@ Item {
|
||||
property bool requestDockShow: false
|
||||
property int pinnedAppCount: 0
|
||||
property bool groupByApp: false
|
||||
property bool isVertical: false
|
||||
|
||||
implicitWidth: row.width
|
||||
implicitHeight: row.height
|
||||
implicitWidth: isVertical ? appLayout.height : appLayout.width
|
||||
implicitHeight: isVertical ? appLayout.width : appLayout.height
|
||||
|
||||
function movePinnedApp(fromIndex, toIndex) {
|
||||
if (fromIndex === toIndex) {
|
||||
@@ -33,11 +34,16 @@ Item {
|
||||
SessionData.setPinnedApps(currentPinned)
|
||||
}
|
||||
|
||||
Row {
|
||||
id: row
|
||||
spacing: 8
|
||||
Item {
|
||||
id: appLayout
|
||||
anchors.centerIn: parent
|
||||
height: 40
|
||||
width: layoutFlow.width
|
||||
height: layoutFlow.height
|
||||
|
||||
Flow {
|
||||
id: layoutFlow
|
||||
flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
||||
spacing: 8
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
@@ -218,6 +224,7 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
||||
@@ -92,24 +92,41 @@ PanelWindow {
|
||||
}
|
||||
|
||||
const dockBackground = findDockBackground(dockWindow.contentItem)
|
||||
let actualDockWidth = dockWindow.width
|
||||
if (dockBackground) {
|
||||
actualDockHeight = dockBackground.height
|
||||
actualDockWidth = dockBackground.width
|
||||
}
|
||||
|
||||
const isDockAtBottom = SettingsData.dockPosition === SettingsData.Position.Bottom
|
||||
const dockBottomMargin = 16
|
||||
let buttonScreenY
|
||||
const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
|
||||
const dockMargin = 16
|
||||
let buttonScreenX, buttonScreenY
|
||||
|
||||
if (isDockAtBottom) {
|
||||
buttonScreenY = root.screen.height - actualDockHeight - dockBottomMargin - 20
|
||||
if (isVertical) {
|
||||
const dockContentHeight = dockWindow.height
|
||||
const screenHeight = root.screen.height
|
||||
const dockTopMargin = Math.round((screenHeight - dockContentHeight) / 2)
|
||||
buttonScreenY = dockTopMargin + buttonPosInDock.y + anchorItem.height / 2
|
||||
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Right) {
|
||||
buttonScreenX = root.screen.width - actualDockWidth - dockMargin - 20
|
||||
} else {
|
||||
buttonScreenX = actualDockWidth + dockMargin + 20
|
||||
}
|
||||
} else {
|
||||
buttonScreenY = actualDockHeight + dockBottomMargin + 20
|
||||
}
|
||||
const isDockAtBottom = SettingsData.dockPosition === SettingsData.Position.Bottom
|
||||
|
||||
const dockContentWidth = dockWindow.width
|
||||
const screenWidth = root.screen.width
|
||||
const dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2)
|
||||
const buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2
|
||||
if (isDockAtBottom) {
|
||||
buttonScreenY = root.screen.height - actualDockHeight - dockMargin - 20
|
||||
} else {
|
||||
buttonScreenY = actualDockHeight + dockMargin + 20
|
||||
}
|
||||
|
||||
const dockContentWidth = dockWindow.width
|
||||
const screenWidth = root.screen.width
|
||||
const dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2)
|
||||
buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2
|
||||
}
|
||||
|
||||
anchorPos = Qt.point(buttonScreenX, buttonScreenY)
|
||||
}
|
||||
@@ -121,17 +138,35 @@ PanelWindow {
|
||||
height: Math.max(60, menuColumn.implicitHeight + Theme.spacingS * 2)
|
||||
|
||||
x: {
|
||||
const left = 10
|
||||
const right = root.width - width - 10
|
||||
const want = root.anchorPos.x - width / 2
|
||||
return Math.max(left, Math.min(right, want))
|
||||
const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
|
||||
if (isVertical) {
|
||||
const isDockAtRight = SettingsData.dockPosition === SettingsData.Position.Right
|
||||
if (isDockAtRight) {
|
||||
return Math.max(10, root.anchorPos.x - width + 30)
|
||||
} else {
|
||||
return Math.min(root.width - width - 10, root.anchorPos.x - 30)
|
||||
}
|
||||
} else {
|
||||
const left = 10
|
||||
const right = root.width - width - 10
|
||||
const want = root.anchorPos.x - width / 2
|
||||
return Math.max(left, Math.min(right, want))
|
||||
}
|
||||
}
|
||||
y: {
|
||||
const isDockAtBottom = SettingsData.dockPosition === SettingsData.Position.Bottom
|
||||
if (isDockAtBottom) {
|
||||
return Math.max(10, root.anchorPos.y - height + 30)
|
||||
const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
|
||||
if (isVertical) {
|
||||
const top = 10
|
||||
const bottom = root.height - height - 10
|
||||
const want = root.anchorPos.y - height / 2
|
||||
return Math.max(top, Math.min(bottom, want))
|
||||
} else {
|
||||
return Math.min(root.height - height - 10, root.anchorPos.y - 30)
|
||||
const isDockAtBottom = SettingsData.dockPosition === SettingsData.Position.Bottom
|
||||
if (isDockAtBottom) {
|
||||
return Math.max(10, root.anchorPos.y - height + 30)
|
||||
} else {
|
||||
return Math.min(root.height - height - 10, root.anchorPos.y - 30)
|
||||
}
|
||||
}
|
||||
}
|
||||
color: Theme.popupBackground()
|
||||
|
||||
105
Modules/Greetd/GreetdMemory.qml
Normal file
105
Modules/Greetd/GreetdMemory.qml
Normal file
@@ -0,0 +1,105 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
|
||||
readonly property string sessionConfigPath: greetCfgDir + "/session.json"
|
||||
readonly property string memoryFile: greetCfgDir + "/memory.json"
|
||||
|
||||
property string lastSessionId: ""
|
||||
property string lastSuccessfulUser: ""
|
||||
property bool isLightMode: false
|
||||
property bool nightModeEnabled: false
|
||||
|
||||
Component.onCompleted: {
|
||||
Quickshell.execDetached(["mkdir", "-p", greetCfgDir])
|
||||
loadMemory()
|
||||
loadSessionConfig()
|
||||
}
|
||||
|
||||
function loadMemory() {
|
||||
parseMemory(memoryFileView.text())
|
||||
}
|
||||
|
||||
function loadSessionConfig() {
|
||||
parseSessionConfig(sessionConfigFileView.text())
|
||||
}
|
||||
|
||||
function parseSessionConfig(content) {
|
||||
try {
|
||||
if (content && content.trim()) {
|
||||
const config = JSON.parse(content)
|
||||
isLightMode = config.isLightMode !== undefined ? config.isLightMode : false
|
||||
nightModeEnabled = config.nightModeEnabled !== undefined ? config.nightModeEnabled : false
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to parse greeter session config:", e)
|
||||
}
|
||||
}
|
||||
|
||||
function parseMemory(content) {
|
||||
try {
|
||||
if (content && content.trim()) {
|
||||
const memory = JSON.parse(content)
|
||||
lastSessionId = memory.lastSessionId !== undefined ? memory.lastSessionId : ""
|
||||
lastSuccessfulUser = memory.lastSuccessfulUser !== undefined ? memory.lastSuccessfulUser : ""
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to parse greetd memory:", e)
|
||||
}
|
||||
}
|
||||
|
||||
function saveMemory() {
|
||||
memoryFileView.setText(JSON.stringify({
|
||||
"lastSessionId": lastSessionId,
|
||||
"lastSuccessfulUser": lastSuccessfulUser
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
function setLastSessionId(id) {
|
||||
lastSessionId = id || ""
|
||||
saveMemory()
|
||||
}
|
||||
|
||||
function setLastSuccessfulUser(username) {
|
||||
lastSuccessfulUser = username || ""
|
||||
saveMemory()
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: memoryFileView
|
||||
path: root.memoryFile
|
||||
blockLoading: false
|
||||
blockWrites: false
|
||||
atomicWrites: true
|
||||
watchChanges: false
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
parseMemory(memoryFileView.text())
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: sessionConfigFileView
|
||||
path: root.sessionConfigPath
|
||||
blockLoading: false
|
||||
blockWrites: true
|
||||
atomicWrites: false
|
||||
watchChanges: false
|
||||
printErrors: true
|
||||
onLoaded: {
|
||||
parseSessionConfig(sessionConfigFileView.text())
|
||||
}
|
||||
onLoadFailed: error => {
|
||||
console.warn("Could not load greeter session config from", root.sessionConfigPath, "error:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
114
Modules/Greetd/GreetdSettings.qml
Normal file
114
Modules/Greetd/GreetdSettings.qml
Normal file
@@ -0,0 +1,114 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string configPath: {
|
||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
|
||||
return greetCfgDir + "/settings.json"
|
||||
}
|
||||
|
||||
property string currentThemeName: "blue"
|
||||
property bool settingsLoaded: false
|
||||
property string customThemeFile: ""
|
||||
property string matugenScheme: "scheme-tonal-spot"
|
||||
property bool use24HourClock: true
|
||||
property bool useFahrenheit: false
|
||||
property bool nightModeEnabled: false
|
||||
property string weatherLocation: "New York, NY"
|
||||
property string weatherCoordinates: "40.7128,-74.0060"
|
||||
property bool useAutoLocation: false
|
||||
property bool weatherEnabled: true
|
||||
property string iconTheme: "System Default"
|
||||
property bool useOSLogo: false
|
||||
property string osLogoColorOverride: ""
|
||||
property real osLogoBrightness: 0.5
|
||||
property real osLogoContrast: 1
|
||||
property string fontFamily: "Inter Variable"
|
||||
property string monoFontFamily: "Fira Code"
|
||||
property int fontWeight: Font.Normal
|
||||
property real fontScale: 1.0
|
||||
property real cornerRadius: 12
|
||||
property string widgetBackgroundColor: "sch"
|
||||
property string surfaceBase: "s"
|
||||
property string lockDateFormat: ""
|
||||
property bool lockScreenShowPowerActions: true
|
||||
property var screenPreferences: ({})
|
||||
property int animationSpeed: 2
|
||||
|
||||
readonly property string defaultFontFamily: "Inter Variable"
|
||||
readonly property string defaultMonoFontFamily: "Fira Code"
|
||||
|
||||
function parseSettings(content) {
|
||||
try {
|
||||
if (content && content.trim()) {
|
||||
const settings = JSON.parse(content)
|
||||
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "blue"
|
||||
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : ""
|
||||
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot"
|
||||
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true
|
||||
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false
|
||||
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false
|
||||
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY"
|
||||
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060"
|
||||
useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false
|
||||
weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true
|
||||
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default"
|
||||
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false
|
||||
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : ""
|
||||
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5
|
||||
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1
|
||||
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : defaultFontFamily
|
||||
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : defaultMonoFontFamily
|
||||
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal
|
||||
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0
|
||||
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12
|
||||
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch"
|
||||
surfaceBase = settings.surfaceBase !== undefined ? settings.surfaceBase : "s"
|
||||
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : ""
|
||||
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true
|
||||
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({})
|
||||
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2
|
||||
settingsLoaded = true
|
||||
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.applyGreeterTheme(currentThemeName)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to parse greetd settings:", e)
|
||||
}
|
||||
}
|
||||
|
||||
function getEffectiveLockDateFormat() {
|
||||
return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat
|
||||
}
|
||||
|
||||
function getFilteredScreens(componentId) {
|
||||
const prefs = screenPreferences && screenPreferences[componentId] || ["all"]
|
||||
if (prefs.includes("all")) {
|
||||
return Quickshell.screens
|
||||
}
|
||||
return Quickshell.screens.filter(screen => prefs.includes(screen.name))
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: settingsFile
|
||||
path: root.configPath
|
||||
blockLoading: false
|
||||
blockWrites: true
|
||||
atomicWrites: false
|
||||
watchChanges: false
|
||||
printErrors: true
|
||||
onLoaded: {
|
||||
parseSettings(settingsFile.text())
|
||||
}
|
||||
}
|
||||
}
|
||||
1361
Modules/Greetd/GreeterContent.qml
Normal file
1361
Modules/Greetd/GreeterContent.qml
Normal file
File diff suppressed because it is too large
Load Diff
29
Modules/Greetd/GreeterState.qml
Normal file
29
Modules/Greetd/GreeterState.qml
Normal file
@@ -0,0 +1,29 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string passwordBuffer: ""
|
||||
property string username: ""
|
||||
property string usernameInput: ""
|
||||
property bool showPasswordInput: false
|
||||
property string selectedSession: ""
|
||||
property string pamState: ""
|
||||
property bool unlocking: false
|
||||
|
||||
property var sessionList: []
|
||||
property var sessionExecs: []
|
||||
property var sessionPaths: []
|
||||
property int currentSessionIndex: 0
|
||||
|
||||
function reset() {
|
||||
showPasswordInput = false
|
||||
username = ""
|
||||
usernameInput = ""
|
||||
passwordBuffer = ""
|
||||
pamState = ""
|
||||
}
|
||||
}
|
||||
18
Modules/Greetd/GreeterSurface.qml
Normal file
18
Modules/Greetd/GreeterSurface.qml
Normal file
@@ -0,0 +1,18 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Greetd
|
||||
|
||||
WlSessionLockSurface {
|
||||
id: root
|
||||
|
||||
required property WlSessionLock lock
|
||||
|
||||
color: "transparent"
|
||||
|
||||
GreeterContent {
|
||||
anchors.fill: parent
|
||||
screenName: root.screen?.name ?? ""
|
||||
sessionLock: root.lock
|
||||
}
|
||||
}
|
||||
79
Modules/Greetd/README.md
Normal file
79
Modules/Greetd/README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Dank (dms) Greeter
|
||||
|
||||
A greeter for [greetd](https://github.com/kennylevinsen/greetd) that follows the aesthetics of the dms lock screen.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi user**: Login with any system user
|
||||
- **dms sync**: Sync settings with dms for consistent styling between shell and greeter
|
||||
- **niri or Hyprland**: Use either niri or Hyprland for the greeter's compositor.
|
||||
- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/dankshell`
|
||||
- **Session Memory**: Remembers last selected session and user
|
||||
|
||||
## Installation
|
||||
|
||||
The easiest thing is to run `dms greeter install` or `dms` for interactive installation.
|
||||
|
||||
Manual installation:
|
||||
1. Install `greetd` (in most distro's standard repositories)
|
||||
2. Copy `assets/dms-niri.kdl` or `assets/dms-hypr.conf` to `/etc/greetd`
|
||||
- niri if you want to run the greeter under niri, hypr if you want to run the greeter under Hyprland
|
||||
3. Copy `assets/greet-niri.sh` or `assets/greet-hyprland.sh` to `/usr/local/bin/start-dms-greetd.sh`
|
||||
4. Edit `/etc/greetd/dms-niri.kdl` or `/etc/greetd/dms-hypr.conf` and replace `_DMS_PATH_` with the absolute path to dms, e.g. `/home/joecool/.config/quickshell/dms`
|
||||
5. Edit or create `/etc/greetd/config.toml`
|
||||
```toml
|
||||
[terminal]
|
||||
# The VT to run the greeter on. Can be "next", "current" or a number
|
||||
# designating the VT.
|
||||
vt = 1
|
||||
|
||||
# The default session, also known as the greeter.
|
||||
[default_session]
|
||||
|
||||
# `agreety` is the bundled agetty/login-lookalike. You can replace `/bin/sh`
|
||||
# with whatever you want started, such as `sway`.
|
||||
|
||||
# The user to run the command as. The privileges this user must have depends
|
||||
# on the greeter. A graphical greeter may for example require the user to be
|
||||
# in the `video` group.
|
||||
user = "greeter"
|
||||
|
||||
command = "/usr/local/bin/start-dms-greetd.sh"
|
||||
```
|
||||
|
||||
Enable the greeter with `sudo systemctl enable greetd`
|
||||
|
||||
## Usage
|
||||
|
||||
To run dms in greeter mode you just need to set `DMS_RUN_GREETER=1` in the environment.
|
||||
|
||||
```bash
|
||||
DMS_RUN_GREETER=1 qs -p /path/to/dms
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
#### Compositor
|
||||
|
||||
You can configure compositor specific settings such as outputs/displays the same as you would in niri or Hyprland.
|
||||
|
||||
Simply edit `/etc/greetd/dms-niri.kdl` or `/etc/greetd/dms-hypr.conf` to change compositor settings for the greeter
|
||||
|
||||
#### Personalization
|
||||
|
||||
Wallpapers and themes and weather and clock formats and things are a TODO on the documentation, but it's configured exactly the same as dms.
|
||||
|
||||
You can synchronize those configurations with a specific user if you want greeter settings to always mirror the shell.
|
||||
|
||||
```bash
|
||||
# For core settings (theme, clock formats, etc)
|
||||
sudo ln -sf ~/.config/DankMaterialShell/settings.json /etc/greetd/.dms/settings.json
|
||||
# For state (mainly you would configure wallpaper in this file)
|
||||
sudo ln -sf ~/.local/state/DankMaterialShell/session.json /etc/greetd/.dms/session.json
|
||||
# For wallpaper based theming
|
||||
sudo ln -sf ~/.cache/quickshell/dankshell/dms-colors.json /etc/greetd/.dms/dms-colors.json
|
||||
```
|
||||
|
||||
You can override the configuration path with the `DMS_GREET_CFG_DIR` environment variable, the default is `/etc/greetd/.dms`
|
||||
|
||||
It should be writable by the greeter user.
|
||||
3
Modules/Greetd/assets/dms-hypr.conf
Normal file
3
Modules/Greetd/assets/dms-hypr.conf
Normal file
@@ -0,0 +1,3 @@
|
||||
env = DMS_RUN_GREETER,1
|
||||
|
||||
exec = sh -c "qs -p _DMS_PATH_; hyprctl dispatch exit"
|
||||
19
Modules/Greetd/assets/dms-niri.kdl
Normal file
19
Modules/Greetd/assets/dms-niri.kdl
Normal file
@@ -0,0 +1,19 @@
|
||||
hotkey-overlay {
|
||||
skip-at-startup
|
||||
}
|
||||
|
||||
environment {
|
||||
DMS_RUN_GREETER "1"
|
||||
}
|
||||
|
||||
spawn-at-startup "sh" "-c" "qs -p _DMS_PATH_; niri msg action quit --skip-confirmation"
|
||||
|
||||
debug {
|
||||
keep-max-bpc-unchanged
|
||||
}
|
||||
|
||||
gestures {
|
||||
hot-corners {
|
||||
off
|
||||
}
|
||||
}
|
||||
8
Modules/Greetd/assets/greet-hyprland.sh
Executable file
8
Modules/Greetd/assets/greet-hyprland.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
export XDG_SESSION_TYPE=wayland
|
||||
export QT_QPA_PLATFORM=wayland
|
||||
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
|
||||
export EGL_PLATFORM=gbm
|
||||
|
||||
exec Hyprland -c /etc/greetd/dms-hypr.conf
|
||||
8
Modules/Greetd/assets/greet-niri.sh
Executable file
8
Modules/Greetd/assets/greet-niri.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
export XDG_SESSION_TYPE=wayland
|
||||
export QT_QPA_PLATFORM=wayland
|
||||
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
|
||||
export EGL_PLATFORM=gbm
|
||||
|
||||
exec niri -c /etc/greetd/dms-niri.kdl
|
||||
@@ -20,6 +20,9 @@ Item {
|
||||
property bool unlocking: false
|
||||
property string pamState: ""
|
||||
property string randomFact: ""
|
||||
property string hyprlandCurrentLayout: ""
|
||||
property string hyprlandKeyboard: ""
|
||||
property int hyprlandLayoutCount: 0
|
||||
|
||||
signal unlockRequested
|
||||
|
||||
@@ -44,10 +47,8 @@ Item {
|
||||
powerDialogVisible = false
|
||||
}
|
||||
|
||||
property var facts: ["A photon takes 100,000 to 200,000 years bouncing through the Sun's dense core, then races to Earth in just 8 minutes 20 seconds.", "A teaspoon of neutron star matter would weigh a billion metric tons here on Earth.", "Right now, 100 trillion solar neutrinos are passing through your body every second.", "The Sun converts 4 million metric tons of matter into pure energy every second—enough to power Earth for 500,000 years.", "The universe still glows with leftover heat from the Big Bang—just 2.7 degrees above absolute zero.", "There's a nebula out there that's actually colder than empty space itself.", "We've detected black holes crashing together by measuring spacetime stretch by less than 1/10,000th the width of a proton.", "Fast radio bursts can release more energy in 5 milliseconds than our Sun produces in 3 days.", "Our galaxy might be crawling with billions of rogue planets drifting alone in the dark.", "Distant galaxies can move away from us faster than light because space itself is stretching.", "The edge of what we can see is 46.5 billion light-years away, even though the universe is only 13.8 billion years old.", "The universe is mostly invisible: 5% regular matter, 27% dark matter, 68% dark energy.", "A day on Venus lasts longer than its entire year around the Sun.", "On Mercury, the time between sunrises is 176 Earth days long.", "In about 4.5 billion years, our galaxy will smash into Andromeda.", "Most of the gold in your jewelry was forged when neutron stars collided somewhere in space.", "PSR J1748-2446ad, the fastest spinning star, rotates 716 times per second—its equator moves at 24% the speed of light.", "Cosmic rays create particles that shouldn't make it to Earth's surface, but time dilation lets them sneak through.", "Jupiter's magnetic field is so huge that if we could see it, it would look bigger than the Moon in our sky.", "Interstellar space is so empty it's like a cube 32 kilometers wide containing just a single grain of sand.", "Voyager 1 is 24 billion kilometers away but won't leave the Sun's gravitational influence for another 30,000 years.", "Counting to a billion at one number per second would take over 31 years.", "Space is so vast, even speeding at light-speed, you'd never return past the cosmic horizon.", "Astronauts on the ISS age about 0.01 seconds less each year than people on Earth.", "Sagittarius B2, a dust cloud near our galaxy's center, contains ethyl formate—the compound that gives raspberries their flavor and rum its smell.", "Beyond 16 billion light-years, the cosmic event horizon marks where space expands too fast for light to ever reach us again.", "Even at light-speed, you'd never catch up to most galaxies—space expands faster.", "Only around 5% of galaxies are ever reachable—even at light-speed.", "If the Sun vanished, we'd still orbit it for 8 minutes before drifting away.", "If a planet 65 million light-years away looked at Earth now, it'd see dinosaurs.", "Our oldest radio signals will reach the Milky Way's center in 26,000 years.", "Every atom in your body heavier than hydrogen was forged in the nuclear furnace of a dying star.", "The Moon moves 3.8 centimeters farther from Earth every year.", "The universe creates 275 million new stars every single day.", "Jupiter's Great Red Spot is a storm twice the size of Earth that has been raging for at least 350 years.", "If you watched someone fall into a black hole, they'd appear frozen at the event horizon forever—time effectively stops from your perspective.", "The Boötes Supervoid is a cosmic desert 1.8 billion light-years across with 60% fewer galaxies than it should have."]
|
||||
|
||||
function pickRandomFact() {
|
||||
randomFact = facts[Math.floor(Math.random() * facts.length)]
|
||||
randomFact = Facts.getRandomFact()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -57,6 +58,11 @@ Item {
|
||||
|
||||
WeatherService.addRef()
|
||||
UserInfoService.refreshUserInfo()
|
||||
|
||||
if (CompositorService.isHyprland) {
|
||||
updateHyprlandLayout()
|
||||
hyprlandLayoutUpdateTimer.start()
|
||||
}
|
||||
}
|
||||
onDemoModeChanged: {
|
||||
if (demoMode) {
|
||||
@@ -65,6 +71,56 @@ Item {
|
||||
}
|
||||
Component.onDestruction: {
|
||||
WeatherService.removeRef()
|
||||
if (CompositorService.isHyprland) {
|
||||
hyprlandLayoutUpdateTimer.stop()
|
||||
}
|
||||
}
|
||||
|
||||
function updateHyprlandLayout() {
|
||||
if (CompositorService.isHyprland) {
|
||||
hyprlandLayoutProcess.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: hyprlandLayoutProcess
|
||||
running: false
|
||||
command: ["hyprctl", "-j", "devices"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const data = JSON.parse(text)
|
||||
const mainKeyboard = data.keyboards.find(kb => kb.main === true)
|
||||
hyprlandKeyboard = mainKeyboard.name
|
||||
if (mainKeyboard && mainKeyboard.active_keymap) {
|
||||
const parts = mainKeyboard.active_keymap.split(" ")
|
||||
if (parts.length > 0) {
|
||||
hyprlandCurrentLayout = parts[0].substring(0, 2).toUpperCase()
|
||||
} else {
|
||||
hyprlandCurrentLayout = mainKeyboard.active_keymap.substring(0, 2).toUpperCase()
|
||||
}
|
||||
} else {
|
||||
hyprlandCurrentLayout = ""
|
||||
}
|
||||
if (mainKeyboard && mainKeyboard.layout_names) {
|
||||
hyprlandLayoutCount = mainKeyboard.layout_names.length
|
||||
} else {
|
||||
hyprlandLayoutCount = 0
|
||||
}
|
||||
} catch (e) {
|
||||
hyprlandCurrentLayout = ""
|
||||
hyprlandLayoutCount = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hyprlandLayoutUpdateTimer
|
||||
interval: 1000
|
||||
running: false
|
||||
repeat: true
|
||||
onTriggered: updateHyprlandLayout()
|
||||
}
|
||||
|
||||
Loader {
|
||||
@@ -180,93 +236,21 @@ Item {
|
||||
spacing: Theme.spacingL
|
||||
Layout.fillWidth: true
|
||||
|
||||
Item {
|
||||
id: avatarContainer
|
||||
|
||||
property bool hasImage: profileImageLoader.status === Image.Ready
|
||||
|
||||
DankCircularImage {
|
||||
Layout.preferredWidth: 60
|
||||
Layout.preferredHeight: 60
|
||||
|
||||
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
|
||||
imageSource: {
|
||||
if (PortalService.profileImage === "") {
|
||||
return ""
|
||||
}
|
||||
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: 60 - 10
|
||||
height: 60 - 10
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "black"
|
||||
antialiasing: true
|
||||
if (PortalService.profileImage.startsWith("/")) {
|
||||
return "file://" + PortalService.profileImage
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: Theme.primary
|
||||
visible: !parent.hasImage
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "person"
|
||||
size: Theme.iconSize + 4
|
||||
color: Theme.primaryText
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "warning"
|
||||
size: Theme.iconSize + 4
|
||||
color: Theme.primaryText
|
||||
visible: PortalService.profileImage !== "" && profileImageLoader.status === Image.Error
|
||||
return PortalService.profileImage
|
||||
}
|
||||
fallbackIcon: "person"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -594,7 +578,7 @@ Item {
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: root.pamState ? 20 : 0
|
||||
Layout.preferredHeight: 20
|
||||
text: {
|
||||
if (root.pamState === "error") {
|
||||
return "Authentication error - try again"
|
||||
@@ -610,7 +594,6 @@ Item {
|
||||
color: Theme.error
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: root.pamState !== ""
|
||||
opacity: root.pamState !== "" ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
@@ -619,13 +602,6 @@ Item {
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on Layout.preferredHeight {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,6 +622,92 @@ Item {
|
||||
anchors.margins: Theme.spacingXL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Item {
|
||||
width: keyboardLayoutRow.width
|
||||
height: keyboardLayoutRow.height
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: {
|
||||
if (CompositorService.isNiri) {
|
||||
return NiriService.keyboardLayoutNames.length > 1
|
||||
} else if (CompositorService.isHyprland) {
|
||||
return hyprlandLayoutCount > 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Row {
|
||||
id: keyboardLayoutRow
|
||||
spacing: 4
|
||||
|
||||
Item {
|
||||
width: Theme.iconSize
|
||||
height: Theme.iconSize
|
||||
|
||||
DankIcon {
|
||||
name: "keyboard"
|
||||
size: Theme.iconSize
|
||||
color: "white"
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: childrenRect.width
|
||||
height: Theme.iconSize
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (CompositorService.isNiri) {
|
||||
const layout = NiriService.getCurrentKeyboardLayoutName()
|
||||
if (!layout) return ""
|
||||
const parts = layout.split(" ")
|
||||
if (parts.length > 0) {
|
||||
return parts[0].substring(0, 2).toUpperCase()
|
||||
}
|
||||
return layout.substring(0, 2).toUpperCase()
|
||||
} else if (CompositorService.isHyprland) {
|
||||
return hyprlandCurrentLayout
|
||||
}
|
||||
return ""
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: keyboardLayoutArea
|
||||
anchors.fill: parent
|
||||
enabled: !demoMode
|
||||
hoverEnabled: enabled
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: {
|
||||
if (CompositorService.isNiri) {
|
||||
NiriService.cycleKeyboardLayout()
|
||||
} else if (CompositorService.isHyprland) {
|
||||
Quickshell.execDetached([
|
||||
"hyprctl",
|
||||
"switchxkblayout",
|
||||
hyprlandKeyboard,
|
||||
"next"
|
||||
])
|
||||
updateHyprlandLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 1
|
||||
height: 24
|
||||
color: Qt.rgba(255, 255, 255, 0.2)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: MprisController.activePlayer
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
visible: MprisController.activePlayer
|
||||
@@ -1110,6 +1172,8 @@ Item {
|
||||
return
|
||||
}
|
||||
console.log("Authentication failed:", res)
|
||||
passwordField.text = ""
|
||||
root.passwordBuffer = ""
|
||||
if (res === PamResult.Error)
|
||||
root.pamState = "error"
|
||||
else if (res === PamResult.MaxTries)
|
||||
|
||||
@@ -163,7 +163,7 @@ Item {
|
||||
|
||||
DankDropdown {
|
||||
id: fontDropdown
|
||||
anchors.left: parent.left
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: -Theme.spacingM
|
||||
width: parent.width + Theme.spacingM
|
||||
text: "Font Family"
|
||||
|
||||
@@ -157,7 +157,6 @@ Rectangle {
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: parent.width
|
||||
text: "Low Priority"
|
||||
description: "Timeout for low priority notifications"
|
||||
currentValue: getTimeoutText(SettingsData.notificationTimeoutLow)
|
||||
@@ -173,7 +172,6 @@ Rectangle {
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: parent.width
|
||||
text: "Normal Priority"
|
||||
description: "Timeout for normal priority notifications"
|
||||
currentValue: getTimeoutText(SettingsData.notificationTimeoutNormal)
|
||||
@@ -189,7 +187,6 @@ Rectangle {
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: parent.width
|
||||
text: "Critical Priority"
|
||||
description: "Timeout for critical priority notifications"
|
||||
currentValue: getTimeoutText(SettingsData.notificationTimeoutCritical)
|
||||
|
||||
53
Modules/Plugins/BaseHorizontalPill.qml
Normal file
53
Modules/Plugins/BaseHorizontalPill.qml
Normal file
@@ -0,0 +1,53 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var axis: null
|
||||
property string section: "center"
|
||||
property var popoutTarget: null
|
||||
property var parentScreen: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
property alias content: contentLoader.sourceComponent
|
||||
|
||||
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
|
||||
|
||||
signal clicked()
|
||||
|
||||
width: contentLoader.item ? (contentLoader.item.implicitWidth + horizontalPadding * 2) : 0
|
||||
height: widgetThickness
|
||||
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
|
||||
color: {
|
||||
if (SettingsData.dankBarNoBackground) {
|
||||
return "transparent"
|
||||
}
|
||||
|
||||
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: {
|
||||
if (popoutTarget && popoutTarget.setTriggerPosition) {
|
||||
const globalPos = mapToGlobal(0, 0)
|
||||
const currentScreen = parentScreen || Screen
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, width)
|
||||
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
|
||||
}
|
||||
root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
53
Modules/Plugins/BaseVerticalPill.qml
Normal file
53
Modules/Plugins/BaseVerticalPill.qml
Normal file
@@ -0,0 +1,53 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var axis: null
|
||||
property string section: "center"
|
||||
property var popoutTarget: null
|
||||
property var parentScreen: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
property alias content: contentLoader.sourceComponent
|
||||
|
||||
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
|
||||
|
||||
signal clicked()
|
||||
|
||||
width: widgetThickness
|
||||
height: contentLoader.item ? (contentLoader.item.implicitHeight + horizontalPadding * 2) : 0
|
||||
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
|
||||
color: {
|
||||
if (SettingsData.dankBarNoBackground) {
|
||||
return "transparent"
|
||||
}
|
||||
|
||||
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: {
|
||||
if (popoutTarget && popoutTarget.setTriggerPosition) {
|
||||
const globalPos = mapToGlobal(0, 0)
|
||||
const currentScreen = parentScreen || Screen
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, height)
|
||||
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
|
||||
}
|
||||
root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
132
Modules/Plugins/ListSetting.qml
Normal file
132
Modules/Plugins/ListSetting.qml
Normal file
@@ -0,0 +1,132 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
required property string settingKey
|
||||
required property string label
|
||||
property string description: ""
|
||||
property var defaultValue: []
|
||||
property var items: defaultValue
|
||||
property Component delegate: null
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Component.onCompleted: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
items = settings.loadValue(settingKey, defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
onItemsChanged: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
settings.saveValue(settingKey, items)
|
||||
}
|
||||
}
|
||||
|
||||
function findSettings() {
|
||||
let item = parent
|
||||
while (item) {
|
||||
if (item.saveValue !== undefined && item.loadValue !== undefined) {
|
||||
return item
|
||||
}
|
||||
item = item.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function addItem(item) {
|
||||
items = items.concat([item])
|
||||
}
|
||||
|
||||
function removeItem(index) {
|
||||
const newItems = items.slice()
|
||||
newItems.splice(index, 1)
|
||||
items = newItems
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.label
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.description !== ""
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: root.items
|
||||
delegate: root.delegate ? root.delegate : defaultDelegate
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "No items added yet"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: root.items.length === 0
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: defaultDelegate
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.width: 0
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: modelData
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 60
|
||||
height: 28
|
||||
color: removeArea.containsMouse ? Theme.errorHover : Theme.error
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: "Remove"
|
||||
color: Theme.errorText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: removeArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.removeItem(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
231
Modules/Plugins/ListSettingWithInput.qml
Normal file
231
Modules/Plugins/ListSettingWithInput.qml
Normal file
@@ -0,0 +1,231 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
required property string settingKey
|
||||
required property string label
|
||||
property string description: ""
|
||||
property var fields: []
|
||||
property var defaultValue: []
|
||||
property var items: defaultValue
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Component.onCompleted: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
items = settings.loadValue(settingKey, defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
onItemsChanged: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
settings.saveValue(settingKey, items)
|
||||
}
|
||||
}
|
||||
|
||||
function findSettings() {
|
||||
let item = parent
|
||||
while (item) {
|
||||
if (item.saveValue !== undefined && item.loadValue !== undefined) {
|
||||
return item
|
||||
}
|
||||
item = item.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function addItem(item) {
|
||||
items = items.concat([item])
|
||||
}
|
||||
|
||||
function removeItem(index) {
|
||||
const newItems = items.slice()
|
||||
newItems.splice(index, 1)
|
||||
items = newItems
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.label
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.description !== ""
|
||||
}
|
||||
|
||||
Flow {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: root.fields
|
||||
|
||||
StyledText {
|
||||
text: modelData.label
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
width: modelData.width || 200
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
id: inputRow
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
property var inputFields: []
|
||||
|
||||
Repeater {
|
||||
id: inputRepeater
|
||||
model: root.fields
|
||||
|
||||
DankTextField {
|
||||
width: modelData.width || 200
|
||||
placeholderText: modelData.placeholder || ""
|
||||
|
||||
Component.onCompleted: {
|
||||
inputRow.inputFields.push(this)
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
addButton.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankButton {
|
||||
id: addButton
|
||||
width: 50
|
||||
height: 36
|
||||
text: "Add"
|
||||
|
||||
onClicked: {
|
||||
let newItem = {}
|
||||
let hasValue = false
|
||||
|
||||
for (let i = 0; i < root.fields.length; i++) {
|
||||
const field = root.fields[i]
|
||||
const input = inputRow.inputFields[i]
|
||||
const value = input.text.trim()
|
||||
|
||||
if (value !== "") {
|
||||
hasValue = true
|
||||
}
|
||||
|
||||
if (field.required && value === "") {
|
||||
return
|
||||
}
|
||||
|
||||
newItem[field.id] = value || (field.default || "")
|
||||
}
|
||||
|
||||
if (hasValue) {
|
||||
root.addItem(newItem)
|
||||
for (let i = 0; i < inputRow.inputFields.length; i++) {
|
||||
inputRow.inputFields[i].text = ""
|
||||
}
|
||||
if (inputRow.inputFields.length > 0) {
|
||||
inputRow.inputFields[0].forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Current Items"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
visible: root.items.length > 0
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: root.items
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.width: 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Repeater {
|
||||
model: root.fields
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
const value = root.items[index][modelData.id]
|
||||
return value || ""
|
||||
}
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
width: modelData.width || 200
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 60
|
||||
height: 28
|
||||
color: removeArea.containsMouse ? Theme.errorHover : Theme.error
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: "Remove"
|
||||
color: Theme.errorText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: removeArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.removeItem(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "No items added yet"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: root.items.length === 0
|
||||
}
|
||||
}
|
||||
}
|
||||
108
Modules/Plugins/PluginComponent.qml
Normal file
108
Modules/Plugins/PluginComponent.qml
Normal file
@@ -0,0 +1,108 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var axis: null
|
||||
property string section: "center"
|
||||
property var parentScreen: null
|
||||
property real widgetThickness: 30
|
||||
property real barThickness: 48
|
||||
property string pluginId: ""
|
||||
property var pluginService: null
|
||||
|
||||
property Component horizontalBarPill: null
|
||||
property Component verticalBarPill: null
|
||||
property Component popoutContent: null
|
||||
property real popoutWidth: 400
|
||||
property real popoutHeight: 400
|
||||
|
||||
property var pluginData: ({})
|
||||
|
||||
readonly property bool isVertical: axis?.isVertical ?? false
|
||||
readonly property bool hasHorizontalPill: horizontalBarPill !== null
|
||||
readonly property bool hasVerticalPill: verticalBarPill !== null
|
||||
readonly property bool hasPopout: popoutContent !== null
|
||||
|
||||
Component.onCompleted: {
|
||||
loadPluginData()
|
||||
}
|
||||
|
||||
onPluginServiceChanged: {
|
||||
loadPluginData()
|
||||
}
|
||||
|
||||
onPluginIdChanged: {
|
||||
loadPluginData()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: pluginService
|
||||
function onPluginDataChanged(changedPluginId) {
|
||||
if (changedPluginId === pluginId) {
|
||||
loadPluginData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadPluginData() {
|
||||
if (!pluginService || !pluginId) {
|
||||
pluginData = {}
|
||||
return
|
||||
}
|
||||
pluginData = SettingsData.getPluginSettingsForPlugin(pluginId)
|
||||
}
|
||||
|
||||
width: isVertical ? (hasVerticalPill ? verticalPill.width : 0) : (hasHorizontalPill ? horizontalPill.width : 0)
|
||||
height: isVertical ? (hasVerticalPill ? verticalPill.height : 0) : (hasHorizontalPill ? horizontalPill.height : 0)
|
||||
|
||||
BaseHorizontalPill {
|
||||
id: horizontalPill
|
||||
visible: !isVertical && hasHorizontalPill
|
||||
axis: root.axis
|
||||
section: root.section
|
||||
popoutTarget: hasPopout ? pluginPopout : null
|
||||
parentScreen: root.parentScreen
|
||||
widgetThickness: root.widgetThickness
|
||||
barThickness: root.barThickness
|
||||
content: root.horizontalBarPill
|
||||
onClicked: {
|
||||
if (hasPopout) {
|
||||
pluginPopout.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BaseVerticalPill {
|
||||
id: verticalPill
|
||||
visible: isVertical && hasVerticalPill
|
||||
axis: root.axis
|
||||
section: root.section
|
||||
popoutTarget: hasPopout ? pluginPopout : null
|
||||
parentScreen: root.parentScreen
|
||||
widgetThickness: root.widgetThickness
|
||||
barThickness: root.barThickness
|
||||
content: root.verticalBarPill
|
||||
onClicked: {
|
||||
if (hasPopout) {
|
||||
pluginPopout.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closePopout() {
|
||||
if (pluginPopout) {
|
||||
pluginPopout.close()
|
||||
}
|
||||
}
|
||||
|
||||
PluginPopout {
|
||||
id: pluginPopout
|
||||
contentWidth: root.popoutWidth
|
||||
contentHeight: root.popoutHeight
|
||||
pluginContent: root.popoutContent
|
||||
}
|
||||
}
|
||||
87
Modules/Plugins/PluginPopout.qml
Normal file
87
Modules/Plugins/PluginPopout.qml
Normal file
@@ -0,0 +1,87 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankPopout {
|
||||
id: root
|
||||
|
||||
property var triggerScreen: null
|
||||
property Component pluginContent: null
|
||||
property real contentWidth: 400
|
||||
property real contentHeight: 400
|
||||
|
||||
function setTriggerPosition(x, y, width, section, screen) {
|
||||
triggerX = x
|
||||
triggerY = y
|
||||
triggerWidth = width
|
||||
triggerSection = section
|
||||
triggerScreen = screen
|
||||
}
|
||||
|
||||
popupWidth: contentWidth
|
||||
popupHeight: popoutContent.item ? popoutContent.item.implicitHeight : contentHeight
|
||||
screen: triggerScreen
|
||||
shouldBeVisible: false
|
||||
visible: shouldBeVisible
|
||||
|
||||
content: Component {
|
||||
Rectangle {
|
||||
id: popoutContainer
|
||||
|
||||
implicitHeight: popoutColumn.implicitHeight + Theme.spacingL * 2
|
||||
color: Theme.popupBackground()
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 0
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
focus: true
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.shouldBeVisible) {
|
||||
forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (root.shouldBeVisible) {
|
||||
Qt.callLater(() => {
|
||||
popoutContainer.forceActiveFocus()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: popoutColumn
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: parent.height - Theme.spacingS * 2
|
||||
x: Theme.spacingS
|
||||
y: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Loader {
|
||||
id: popoutContent
|
||||
width: parent.width
|
||||
sourceComponent: root.pluginContent
|
||||
|
||||
onLoaded: {
|
||||
if (item && "closePopout" in item) {
|
||||
item.closePopout = function() {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
Modules/Plugins/PluginSettings.qml
Normal file
70
Modules/Plugins/PluginSettings.qml
Normal file
@@ -0,0 +1,70 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string pluginId
|
||||
property var pluginService: null
|
||||
default property alias content: settingsColumn.children
|
||||
|
||||
signal settingChanged()
|
||||
|
||||
implicitHeight: hasPermission ? settingsColumn.implicitHeight : errorText.implicitHeight
|
||||
height: implicitHeight
|
||||
|
||||
readonly property bool hasPermission: pluginService && pluginService.hasPermission ? pluginService.hasPermission(pluginId, "settings_write") : true
|
||||
|
||||
onPluginServiceChanged: {
|
||||
if (pluginService) {
|
||||
for (let i = 0; i < settingsColumn.children.length; i++) {
|
||||
const child = settingsColumn.children[i]
|
||||
if (child.loadValue) {
|
||||
child.loadValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveValue(key, value) {
|
||||
if (!pluginService) {
|
||||
return
|
||||
}
|
||||
if (!hasPermission) {
|
||||
console.warn("PluginSettings: Plugin", pluginId, "does not have settings_write permission")
|
||||
return
|
||||
}
|
||||
if (pluginService.savePluginData) {
|
||||
pluginService.savePluginData(pluginId, key, value)
|
||||
settingChanged()
|
||||
}
|
||||
}
|
||||
|
||||
function loadValue(key, defaultValue) {
|
||||
if (pluginService && pluginService.loadPluginData) {
|
||||
return pluginService.loadPluginData(pluginId, key, defaultValue)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: errorText
|
||||
visible: pluginService && !root.hasPermission
|
||||
anchors.fill: parent
|
||||
text: "This plugin does not have 'settings_write' permission.\n\nAdd \"permissions\": [\"settings_read\", \"settings_write\"] to plugin.json"
|
||||
color: Theme.error
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
id: settingsColumn
|
||||
visible: root.hasPermission
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
}
|
||||
}
|
||||
76
Modules/Plugins/PopoutComponent.qml
Normal file
76
Modules/Plugins/PopoutComponent.qml
Normal file
@@ -0,0 +1,76 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
property string headerText: ""
|
||||
property string detailsText: ""
|
||||
property bool showCloseButton: false
|
||||
property var closePopout: null
|
||||
|
||||
readonly property int headerHeight: popoutHeader.visible ? popoutHeader.height : 0
|
||||
readonly property int detailsHeight: popoutDetails.visible ? popoutDetails.implicitHeight : 0
|
||||
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
id: popoutHeader
|
||||
width: parent.width
|
||||
height: 40
|
||||
visible: headerText.length > 0
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.headerText
|
||||
font.pixelSize: Theme.fontSizeLarge + 4
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: closeButton
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: closeArea.containsMouse ? Theme.errorHover : "transparent"
|
||||
visible: root.showCloseButton
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "close"
|
||||
size: Theme.iconSize - 4
|
||||
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: {
|
||||
if (root.closePopout) {
|
||||
root.closePopout()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: popoutDetails
|
||||
width: parent.width
|
||||
leftPadding: Theme.spacingS
|
||||
bottomPadding: Theme.spacingS
|
||||
text: root.detailsText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: detailsText.length > 0
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
91
Modules/Plugins/SelectionSetting.qml
Normal file
91
Modules/Plugins/SelectionSetting.qml
Normal file
@@ -0,0 +1,91 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
required property string settingKey
|
||||
required property string label
|
||||
property string description: ""
|
||||
required property var options
|
||||
property string defaultValue: ""
|
||||
property string value: defaultValue
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
function loadValue() {
|
||||
const settings = findSettings()
|
||||
if (settings && settings.pluginService) {
|
||||
value = settings.loadValue(settingKey, defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
loadValue()
|
||||
}
|
||||
|
||||
readonly property var optionLabels: {
|
||||
const labels = []
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
labels.push(options[i].label || options[i])
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
readonly property var valueToLabel: {
|
||||
const map = {}
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
const opt = options[i]
|
||||
if (typeof opt === 'object') {
|
||||
map[opt.value] = opt.label
|
||||
} else {
|
||||
map[opt] = opt
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
readonly property var labelToValue: {
|
||||
const map = {}
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
const opt = options[i]
|
||||
if (typeof opt === 'object') {
|
||||
map[opt.label] = opt.value
|
||||
} else {
|
||||
map[opt] = opt
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
onValueChanged: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
settings.saveValue(settingKey, value)
|
||||
}
|
||||
}
|
||||
|
||||
function findSettings() {
|
||||
let item = parent
|
||||
while (item) {
|
||||
if (item.saveValue !== undefined && item.loadValue !== undefined) {
|
||||
return item
|
||||
}
|
||||
item = item.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: parent.width
|
||||
text: root.label
|
||||
description: root.description
|
||||
currentValue: root.valueToLabel[root.value] || root.value
|
||||
options: root.optionLabels
|
||||
onValueChanged: newValue => {
|
||||
root.value = root.labelToValue[newValue] || newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
81
Modules/Plugins/SliderSetting.qml
Normal file
81
Modules/Plugins/SliderSetting.qml
Normal file
@@ -0,0 +1,81 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
required property string settingKey
|
||||
required property string label
|
||||
property string description: ""
|
||||
property int defaultValue: 0
|
||||
property int value: defaultValue
|
||||
property int minimum: 0
|
||||
property int maximum: 100
|
||||
property string leftIcon: ""
|
||||
property string rightIcon: ""
|
||||
property string unit: ""
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
function loadValue() {
|
||||
const settings = findSettings()
|
||||
if (settings && settings.pluginService) {
|
||||
value = settings.loadValue(settingKey, defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
loadValue()
|
||||
}
|
||||
|
||||
onValueChanged: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
settings.saveValue(settingKey, value)
|
||||
}
|
||||
}
|
||||
|
||||
function findSettings() {
|
||||
let item = parent
|
||||
while (item) {
|
||||
if (item.saveValue !== undefined && item.loadValue !== undefined) {
|
||||
return item
|
||||
}
|
||||
item = item.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.label
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.description !== ""
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
width: parent.width
|
||||
value: root.value
|
||||
minimum: root.minimum
|
||||
maximum: root.maximum
|
||||
leftIcon: root.leftIcon
|
||||
rightIcon: root.rightIcon
|
||||
unit: root.unit
|
||||
wheelEnabled: false
|
||||
thumbOutlineColor: Theme.surfaceContainerHighest
|
||||
onSliderValueChanged: newValue => {
|
||||
root.value = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
65
Modules/Plugins/StringSetting.qml
Normal file
65
Modules/Plugins/StringSetting.qml
Normal file
@@ -0,0 +1,65 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
required property string settingKey
|
||||
required property string label
|
||||
property string description: ""
|
||||
property string placeholder: ""
|
||||
property string defaultValue: ""
|
||||
property string value: defaultValue
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Component.onCompleted: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
value = settings.loadValue(settingKey, defaultValue)
|
||||
textField.text = value
|
||||
}
|
||||
}
|
||||
|
||||
function findSettings() {
|
||||
let item = parent
|
||||
while (item) {
|
||||
if (item.saveValue !== undefined && item.loadValue !== undefined) {
|
||||
return item
|
||||
}
|
||||
item = item.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.label
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.description !== ""
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: textField
|
||||
width: parent.width
|
||||
placeholderText: root.placeholder
|
||||
onEditingFinished: {
|
||||
root.value = text
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
settings.saveValue(settingKey, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
Modules/Plugins/ToggleSetting.qml
Normal file
72
Modules/Plugins/ToggleSetting.qml
Normal file
@@ -0,0 +1,72 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
id: root
|
||||
|
||||
required property string settingKey
|
||||
required property string label
|
||||
property string description: ""
|
||||
property bool defaultValue: false
|
||||
property bool value: defaultValue
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Component.onCompleted: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
value = settings.loadValue(settingKey, defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
onValueChanged: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
settings.saveValue(settingKey, value)
|
||||
}
|
||||
}
|
||||
|
||||
function findSettings() {
|
||||
let item = parent
|
||||
while (item) {
|
||||
if (item.saveValue !== undefined && item.loadValue !== undefined) {
|
||||
return item
|
||||
}
|
||||
item = item.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - toggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: root.label
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.description !== ""
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: toggle
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: root.value
|
||||
onToggled: isChecked => {
|
||||
root.value = isChecked
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,17 @@ import qs.Widgets
|
||||
Item {
|
||||
id: dankBarTab
|
||||
|
||||
property var baseWidgetDefinitions: [{
|
||||
function getWidgetsForPopup() {
|
||||
return baseWidgetDefinitions.filter(widget => {
|
||||
if (widget.warning && widget.warning.includes("Plugin is disabled")) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
property var baseWidgetDefinitions: {
|
||||
var coreWidgets = [{
|
||||
"id": "launcherButton",
|
||||
"text": "App Launcher",
|
||||
"description": "Quick access to application launcher",
|
||||
@@ -177,6 +187,24 @@ Item {
|
||||
"icon": "update",
|
||||
"enabled": SystemUpdateService.distributionSupported
|
||||
}]
|
||||
|
||||
// Add all available plugins (loaded and unloaded)
|
||||
var allPlugins = PluginService.getAvailablePlugins()
|
||||
for (var i = 0; i < allPlugins.length; i++) {
|
||||
var plugin = allPlugins[i]
|
||||
var isLoaded = PluginService.isPluginLoaded(plugin.id)
|
||||
coreWidgets.push({
|
||||
"id": plugin.id,
|
||||
"text": plugin.name,
|
||||
"description": plugin.description || "Plugin widget",
|
||||
"icon": plugin.icon || "extension",
|
||||
"enabled": isLoaded,
|
||||
"warning": !isLoaded ? "Plugin is disabled - enable in Plugins settings to use" : undefined
|
||||
})
|
||||
}
|
||||
|
||||
return coreWidgets
|
||||
}
|
||||
property var defaultLeftWidgets: [{
|
||||
"id": "launcherButton",
|
||||
"enabled": true
|
||||
@@ -1129,7 +1157,7 @@ Item {
|
||||
}
|
||||
onAddWidget: sectionId => {
|
||||
widgetSelectionPopup.allWidgets
|
||||
= dankBarTab.baseWidgetDefinitions
|
||||
= dankBarTab.getWidgetsForPopup()
|
||||
widgetSelectionPopup.targetSection = sectionId
|
||||
widgetSelectionPopup.safeOpen()
|
||||
}
|
||||
@@ -1201,7 +1229,7 @@ Item {
|
||||
}
|
||||
onAddWidget: sectionId => {
|
||||
widgetSelectionPopup.allWidgets
|
||||
= dankBarTab.baseWidgetDefinitions
|
||||
= dankBarTab.getWidgetsForPopup()
|
||||
widgetSelectionPopup.targetSection = sectionId
|
||||
widgetSelectionPopup.safeOpen()
|
||||
}
|
||||
@@ -1273,7 +1301,7 @@ Item {
|
||||
}
|
||||
onAddWidget: sectionId => {
|
||||
widgetSelectionPopup.allWidgets
|
||||
= dankBarTab.baseWidgetDefinitions
|
||||
= dankBarTab.getWidgetsForPopup()
|
||||
widgetSelectionPopup.targetSection = sectionId
|
||||
widgetSelectionPopup.safeOpen()
|
||||
}
|
||||
|
||||
@@ -65,11 +65,24 @@ Item {
|
||||
DankButtonGroup {
|
||||
id: positionButtonGroup
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
model: ["Top", "Bottom"]
|
||||
currentIndex: SettingsData.dockPosition === SettingsData.Position.Bottom ? 1 : 0
|
||||
model: ["Top", "Bottom", "Left", "Right"]
|
||||
currentIndex: {
|
||||
switch (SettingsData.dockPosition) {
|
||||
case SettingsData.Position.Top: return 0
|
||||
case SettingsData.Position.Bottom: return 1
|
||||
case SettingsData.Position.Left: return 2
|
||||
case SettingsData.Position.Right: return 3
|
||||
default: return 1
|
||||
}
|
||||
}
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (selected) {
|
||||
SettingsData.setDockPosition(index === 1 ? SettingsData.Position.Bottom : SettingsData.Position.Top)
|
||||
switch (index) {
|
||||
case 0: SettingsData.setDockPosition(SettingsData.Position.Top); break
|
||||
case 1: SettingsData.setDockPosition(SettingsData.Position.Bottom); break
|
||||
case 2: SettingsData.setDockPosition(SettingsData.Position.Left); break
|
||||
case 3: SettingsData.setDockPosition(SettingsData.Position.Right); break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,7 +183,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Display a dock with pinned and running applications that can be positioned at the top or bottom of the screen"
|
||||
text: "Display a dock with pinned and running applications that can be positioned at the top, bottom, left, or right edge of the screen"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
@@ -520,7 +520,6 @@ Item {
|
||||
DankDropdown {
|
||||
id: monitorDropdown
|
||||
|
||||
width: parent.width - parent.leftPadding
|
||||
text: "Monitor"
|
||||
description: "Select monitor to configure wallpaper"
|
||||
currentValue: selectedMonitorName || "No monitors"
|
||||
@@ -678,7 +677,6 @@ Item {
|
||||
property var intervalOptions: ["1 minute", "5 minutes", "15 minutes", "30 minutes", "1 hour", "1.5 hours", "2 hours", "3 hours", "4 hours", "6 hours", "8 hours", "12 hours"]
|
||||
property var intervalValues: [60, 300, 900, 1800, 3600, 5400, 7200, 10800, 14400, 21600, 28800, 43200]
|
||||
|
||||
width: parent.width - parent.leftPadding
|
||||
visible: {
|
||||
if (SessionData.perMonitorWallpaper) {
|
||||
return SessionData.getMonitorCyclingSettings(selectedMonitorName).mode === "interval"
|
||||
@@ -833,7 +831,6 @@ Item {
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: parent.width
|
||||
text: "Transition Effect"
|
||||
description: "Visual effect used when wallpaper changes"
|
||||
currentValue: {
|
||||
@@ -851,8 +848,6 @@ Item {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: SessionData.wallpaperTransition === "random"
|
||||
leftPadding: Theme.spacingM
|
||||
rightPadding: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: "Include Transitions"
|
||||
@@ -866,12 +861,12 @@ Item {
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width - parent.leftPadding - parent.rightPadding
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
DankButtonGroup {
|
||||
id: transitionGroup
|
||||
width: parent.width - parent.leftPadding - parent.rightPadding
|
||||
width: parent.width
|
||||
selectionMode: "multi"
|
||||
model: SessionData.availableWallpaperTransitions.filter(t => t !== "none")
|
||||
initialSelection: SessionData.includedTransitions
|
||||
@@ -959,7 +954,6 @@ Item {
|
||||
|
||||
DankDropdown {
|
||||
id: personalizationMatugenPaletteDropdown
|
||||
width: parent.width
|
||||
text: "Matugen Palette"
|
||||
description: "Select the palette algorithm used for wallpaper-based colors"
|
||||
options: Theme.availableMatugenSchemes.map(function (option) { return option.label })
|
||||
@@ -1075,7 +1069,6 @@ Item {
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: parent.width
|
||||
text: "Temperature"
|
||||
description: "Color temperature for night mode"
|
||||
currentValue: SessionData.nightModeTemperature + "K"
|
||||
@@ -1424,7 +1417,6 @@ Item {
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: parent.width
|
||||
text: "Popup Position"
|
||||
description: "Choose where notification popups appear on screen"
|
||||
currentValue: {
|
||||
@@ -1506,7 +1498,6 @@ Item {
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: parent.width
|
||||
text: "Font Family"
|
||||
description: "Select system font family"
|
||||
currentValue: {
|
||||
@@ -1528,7 +1519,6 @@ Item {
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: parent.width
|
||||
text: "Font Weight"
|
||||
description: "Select font weight"
|
||||
currentValue: {
|
||||
@@ -1595,7 +1585,6 @@ Item {
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: parent.width
|
||||
text: "Monospace Font"
|
||||
description: "Select monospace font for process list and technical displays"
|
||||
currentValue: {
|
||||
|
||||
446
Modules/Settings/PluginsTab.qml
Normal file
446
Modules/Settings/PluginsTab.qml
Normal file
@@ -0,0 +1,446 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: pluginsTab
|
||||
|
||||
property string expandedPluginId: ""
|
||||
|
||||
|
||||
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: headerColumn.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: headerColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "extension"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: "Plugin Management"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Manage and configure plugins for extending DMS functionality"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankButton {
|
||||
text: "Scan for Plugins"
|
||||
iconName: "refresh"
|
||||
onClicked: {
|
||||
PluginService.scanPlugins()
|
||||
ToastService.showInfo("Scanning for plugins...")
|
||||
}
|
||||
}
|
||||
|
||||
DankButton {
|
||||
text: "Create Plugin Directory"
|
||||
iconName: "create_new_folder"
|
||||
onClicked: {
|
||||
PluginService.createPluginDirectory()
|
||||
ToastService.showInfo("Created plugin directory: " + PluginService.pluginDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: directoryColumn.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: directoryColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: "Plugin Directory"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: PluginService.pluginDirectory
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
font.family: "monospace"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Place plugin directories here. Each plugin should have a plugin.json manifest file."
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: Math.max(200, availableColumn.implicitHeight + Theme.spacingL * 2)
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: availableColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: "Available Plugins"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Repeater {
|
||||
id: pluginRepeater
|
||||
model: PluginService.getAvailablePlugins()
|
||||
|
||||
StyledRect {
|
||||
id: pluginDelegate
|
||||
width: parent.width
|
||||
height: pluginItemColumn.implicitHeight + Theme.spacingM * 2 + settingsContainer.height
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
property var pluginData: modelData
|
||||
property string pluginId: pluginData ? pluginData.id : ""
|
||||
property string pluginName: pluginData ? (pluginData.name || pluginData.id) : ""
|
||||
property string pluginVersion: pluginData ? (pluginData.version || "1.0.0") : ""
|
||||
property string pluginAuthor: pluginData ? (pluginData.author || "Unknown") : ""
|
||||
property string pluginDescription: pluginData ? (pluginData.description || "") : ""
|
||||
property string pluginIcon: pluginData ? (pluginData.icon || "extension") : "extension"
|
||||
property string pluginSettingsPath: pluginData ? (pluginData.settingsPath || "") : ""
|
||||
property var pluginPermissions: pluginData ? (pluginData.permissions || []) : []
|
||||
property bool hasSettings: pluginData && pluginData.settings !== undefined && pluginData.settings !== ""
|
||||
property bool isExpanded: pluginsTab.expandedPluginId === pluginId
|
||||
|
||||
|
||||
color: pluginMouseArea.containsMouse ? Theme.surfacePressed : (isExpanded ? Theme.surfaceContainerHighest : Theme.surfaceContainerHigh)
|
||||
border.width: 0
|
||||
|
||||
MouseArea {
|
||||
id: pluginMouseArea
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: pluginDelegate.isExpanded ? settingsContainer.height : 0
|
||||
hoverEnabled: true
|
||||
cursorShape: pluginDelegate.hasSettings ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: {
|
||||
if (pluginDelegate.hasSettings) {
|
||||
pluginsTab.expandedPluginId = pluginsTab.expandedPluginId === pluginDelegate.pluginId ? "" : pluginDelegate.pluginId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: pluginItemColumn
|
||||
width: parent.width
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: pluginDelegate.pluginIcon
|
||||
size: Theme.iconSize
|
||||
color: PluginService.isPluginLoaded(pluginDelegate.pluginId) ? Theme.primary : Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM - toggleRow.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
width: parent.width
|
||||
|
||||
StyledText {
|
||||
text: pluginDelegate.pluginName
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: pluginDelegate.hasSettings ? (pluginDelegate.isExpanded ? "expand_less" : "expand_more") : ""
|
||||
size: 16
|
||||
color: pluginDelegate.hasSettings ? Theme.primary : "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: pluginDelegate.hasSettings
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "v" + pluginDelegate.pluginVersion + " by " + pluginDelegate.pluginAuthor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: toggleRow
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 14
|
||||
color: reloadArea.containsMouse ? Theme.surfaceContainerHighest : "transparent"
|
||||
visible: PluginService.isPluginLoaded(pluginDelegate.pluginId)
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "refresh"
|
||||
size: 16
|
||||
color: reloadArea.containsMouse ? Theme.primary : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: reloadArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
const currentPluginId = pluginDelegate.pluginId
|
||||
const currentPluginName = pluginDelegate.pluginName
|
||||
pluginsTab.isReloading = true
|
||||
if (PluginService.reloadPlugin(currentPluginId)) {
|
||||
ToastService.showInfo("Plugin reloaded: " + currentPluginName)
|
||||
} else {
|
||||
ToastService.showError("Failed to reload plugin: " + currentPluginName)
|
||||
pluginsTab.isReloading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: pluginToggle
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: PluginService.isPluginLoaded(pluginDelegate.pluginId)
|
||||
onToggled: isChecked => {
|
||||
const currentPluginId = pluginDelegate.pluginId
|
||||
const currentPluginName = pluginDelegate.pluginName
|
||||
|
||||
if (isChecked) {
|
||||
if (PluginService.enablePlugin(currentPluginId)) {
|
||||
ToastService.showInfo("Plugin enabled: " + currentPluginName)
|
||||
} else {
|
||||
ToastService.showError("Failed to enable plugin: " + currentPluginName)
|
||||
checked = false
|
||||
}
|
||||
} else {
|
||||
if (PluginService.disablePlugin(currentPluginId)) {
|
||||
ToastService.showInfo("Plugin disabled: " + currentPluginName)
|
||||
if (pluginDelegate.isExpanded) {
|
||||
pluginsTab.expandedPluginId = ""
|
||||
}
|
||||
} else {
|
||||
ToastService.showError("Failed to disable plugin: " + currentPluginName)
|
||||
checked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: pluginDelegate.pluginDescription
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
visible: pluginDelegate.pluginDescription !== ""
|
||||
}
|
||||
|
||||
Flow {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
visible: pluginDelegate.pluginPermissions && Array.isArray(pluginDelegate.pluginPermissions) && pluginDelegate.pluginPermissions.length > 0
|
||||
|
||||
Repeater {
|
||||
model: pluginDelegate.pluginPermissions
|
||||
|
||||
Rectangle {
|
||||
height: 20
|
||||
width: permissionText.implicitWidth + Theme.spacingXS * 2
|
||||
radius: 10
|
||||
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.3)
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
id: permissionText
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Settings container
|
||||
Item {
|
||||
id: settingsContainer
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: pluginDelegate.isExpanded && pluginDelegate.hasSettings ? (settingsLoader.item ? settingsLoader.item.implicitHeight + Theme.spacingL * 2 : 0) : 0
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.surfaceContainerHighest
|
||||
radius: Theme.cornerRadius
|
||||
anchors.topMargin: Theme.spacingXS
|
||||
border.width: 0
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: settingsLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
active: pluginDelegate.isExpanded && pluginDelegate.hasSettings && PluginService.isPluginLoaded(pluginDelegate.pluginId)
|
||||
asynchronous: false
|
||||
|
||||
source: {
|
||||
if (active && pluginDelegate.pluginSettingsPath) {
|
||||
var path = pluginDelegate.pluginSettingsPath
|
||||
if (!path.startsWith("file://")) {
|
||||
path = "file://" + path
|
||||
}
|
||||
return path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (item && typeof PluginService !== "undefined") {
|
||||
item.pluginService = PluginService
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: !PluginService.isPluginLoaded(pluginDelegate.pluginId) ?
|
||||
"Enable plugin to access settings" :
|
||||
(settingsLoader.status === Loader.Error ?
|
||||
"Failed to load settings" :
|
||||
"No configurable settings")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: pluginDelegate.isExpanded && (!settingsLoader.active || settingsLoader.status === Loader.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: "No plugins found.\nPlace plugins in " + PluginService.pluginDirectory
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: pluginRepeater.model.length === 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property bool isReloading: false
|
||||
|
||||
Connections {
|
||||
target: PluginService
|
||||
function onPluginLoaded() {
|
||||
pluginRepeater.model = PluginService.getAvailablePlugins()
|
||||
if (isReloading) {
|
||||
isReloading = false
|
||||
}
|
||||
}
|
||||
function onPluginUnloaded() {
|
||||
pluginRepeater.model = PluginService.getAvailablePlugins()
|
||||
if (!isReloading && pluginsTab.expandedPluginId !== "" && !PluginService.isPluginLoaded(pluginsTab.expandedPluginId)) {
|
||||
pluginsTab.expandedPluginId = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -651,7 +651,6 @@ Item {
|
||||
|
||||
DankDropdown {
|
||||
id: matugenPaletteDropdown
|
||||
width: parent.width
|
||||
text: "Matugen Palette"
|
||||
description: "Select the palette algorithm used for wallpaper-based colors"
|
||||
options: Theme.availableMatugenSchemes.map(function (option) { return option.label })
|
||||
@@ -993,7 +992,6 @@ Item {
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Icon Theme"
|
||||
description: "DankShell & System Icons\n(requires restart)"
|
||||
|
||||
@@ -121,7 +121,6 @@ Item {
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: parent.width
|
||||
height: 50
|
||||
text: "Top Bar Format"
|
||||
description: "Preview: " + (SettingsData.clockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) : new Date().toLocaleDateString(Qt.locale(), "ddd d"))
|
||||
@@ -185,7 +184,6 @@ Item {
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: parent.width
|
||||
height: 50
|
||||
text: "Lock Screen Format"
|
||||
description: "Preview: " + (SettingsData.lockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.lockDateFormat) : new Date().toLocaleDateString(Qt.locale(), Locale.LongFormat))
|
||||
|
||||
@@ -21,23 +21,23 @@ Popup {
|
||||
filteredWidgets = allWidgets.slice()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
var filtered = []
|
||||
var query = searchQuery.toLowerCase()
|
||||
|
||||
|
||||
for (var i = 0; i < allWidgets.length; i++) {
|
||||
var widget = allWidgets[i]
|
||||
var text = widget.text ? widget.text.toLowerCase() : ""
|
||||
var description = widget.description ? widget.description.toLowerCase() : ""
|
||||
var id = widget.id ? widget.id.toLowerCase() : ""
|
||||
|
||||
if (text.indexOf(query) !== -1 ||
|
||||
description.indexOf(query) !== -1 ||
|
||||
|
||||
if (text.indexOf(query) !== -1 ||
|
||||
description.indexOf(query) !== -1 ||
|
||||
id.indexOf(query) !== -1) {
|
||||
filtered.push(widget)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
filteredWidgets = filtered
|
||||
selectedIndex = -1
|
||||
keyboardNavigationActive = false
|
||||
|
||||
@@ -142,13 +142,14 @@ Column {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Item {
|
||||
width: 120
|
||||
width: 60
|
||||
height: 32
|
||||
visible: modelData.id === "gpuTemp"
|
||||
|
||||
DankDropdown {
|
||||
id: gpuDropdown
|
||||
anchors.fill: parent
|
||||
popupWidth: -1
|
||||
currentValue: {
|
||||
var selectedIndex = modelData.selectedGpuIndex
|
||||
!== undefined ? modelData.selectedGpuIndex : 0
|
||||
@@ -223,12 +224,7 @@ Column {
|
||||
Item {
|
||||
width: 32
|
||||
height: 32
|
||||
visible: (modelData.warning !== undefined
|
||||
&& modelData.warning !== "")
|
||||
&& (modelData.id === "cpuUsage"
|
||||
|| modelData.id === "memUsage"
|
||||
|| modelData.id === "cpuTemp"
|
||||
|| modelData.id === "gpuTemp")
|
||||
visible: modelData.warning !== undefined && modelData.warning !== ""
|
||||
|
||||
DankIcon {
|
||||
name: "warning"
|
||||
|
||||
@@ -11,7 +11,12 @@ LazyLoader {
|
||||
active: true
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("wallpaper")
|
||||
model: {
|
||||
if (SessionData.isGreeterMode) {
|
||||
return Quickshell.screens
|
||||
}
|
||||
return SettingsData.getFilteredScreens("wallpaper")
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: wallpaperWindow
|
||||
|
||||
97
PLUGINS/ExampleEmojiPlugin/EmojiSettings.qml
Normal file
97
PLUGINS/ExampleEmojiPlugin/EmojiSettings.qml
Normal file
@@ -0,0 +1,97 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginSettings {
|
||||
id: root
|
||||
pluginId: "exampleEmojiPlugin"
|
||||
|
||||
// Header section to explain what this plugin does
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: "Emoji Cycler Settings"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: "Configure which emojis appear in your bar, how quickly they cycle, and how many show at once."
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
// Dropdown to select which emoji set to use
|
||||
SelectionSetting {
|
||||
settingKey: "emojiSet"
|
||||
label: "Emoji Set"
|
||||
description: "Choose which collection of emojis to cycle through"
|
||||
options: [
|
||||
{label: "Happy & Sad", value: "happySad"},
|
||||
{label: "Hearts", value: "hearts"},
|
||||
{label: "Hand Gestures", value: "hands"},
|
||||
{label: "All Mixed", value: "mixed"}
|
||||
]
|
||||
defaultValue: "happySad"
|
||||
|
||||
// Update the actual emoji array when selection changes
|
||||
onValueChanged: {
|
||||
const sets = {
|
||||
"happySad": ["😊", "😢", "😂", "😭", "😍", "😡"],
|
||||
"hearts": ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍"],
|
||||
"hands": ["👍", "👎", "👊", "✌️", "🤘", "👌", "✋", "🤚"],
|
||||
"mixed": ["😊", "❤️", "👍", "🎉", "🔥", "✨", "🌟", "💯"]
|
||||
}
|
||||
const newEmojis = sets[value] || sets["happySad"]
|
||||
root.saveValue("emojis", newEmojis)
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Initialize the emojis array on first load
|
||||
const currentSet = value || defaultValue
|
||||
const sets = {
|
||||
"happySad": ["😊", "😢", "😂", "😭", "😍", "😡"],
|
||||
"hearts": ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍"],
|
||||
"hands": ["👍", "👎", "👊", "✌️", "🤘", "👌", "✋", "🤚"],
|
||||
"mixed": ["😊", "❤️", "👍", "🎉", "🔥", "✨", "🌟", "💯"]
|
||||
}
|
||||
const emojis = sets[currentSet] || sets["happySad"]
|
||||
root.saveValue("emojis", emojis)
|
||||
}
|
||||
}
|
||||
|
||||
// Slider to control how fast emojis cycle (in milliseconds)
|
||||
SliderSetting {
|
||||
settingKey: "cycleInterval"
|
||||
label: "Cycle Speed"
|
||||
description: "How quickly emojis rotate (in seconds)"
|
||||
defaultValue: 3000
|
||||
minimum: 500
|
||||
maximum: 10000
|
||||
unit: "ms"
|
||||
leftIcon: "schedule"
|
||||
}
|
||||
|
||||
// Slider to control max emojis shown in the bar
|
||||
SliderSetting {
|
||||
settingKey: "maxBarEmojis"
|
||||
label: "Max Bar Emojis"
|
||||
description: "Maximum number of emojis to display in the bar at once"
|
||||
defaultValue: 3
|
||||
minimum: 1
|
||||
maximum: 8
|
||||
unit: ""
|
||||
rightIcon: "emoji_emotions"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: "💡 Tip: Click the emoji widget in your bar to open the emoji picker and copy any emoji to your clipboard!"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
149
PLUGINS/ExampleEmojiPlugin/EmojiWidget.qml
Normal file
149
PLUGINS/ExampleEmojiPlugin/EmojiWidget.qml
Normal file
@@ -0,0 +1,149 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
id: root
|
||||
|
||||
property var enabledEmojis: pluginData.emojis || ["😊", "😢", "❤️"]
|
||||
property int cycleInterval: pluginData.cycleInterval || 3000
|
||||
property int maxBarEmojis: pluginData.maxBarEmojis || 3
|
||||
|
||||
property int currentIndex: 0
|
||||
property var displayedEmojis: []
|
||||
|
||||
Timer {
|
||||
interval: root.cycleInterval
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (root.enabledEmojis.length > 0) {
|
||||
root.currentIndex = (root.currentIndex + 1) % root.enabledEmojis.length
|
||||
root.updateDisplayedEmojis()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateDisplayedEmojis() {
|
||||
const maxToShow = Math.min(root.maxBarEmojis, root.enabledEmojis.length)
|
||||
let emojis = []
|
||||
for (let i = 0; i < maxToShow; i++) {
|
||||
const idx = (root.currentIndex + i) % root.enabledEmojis.length
|
||||
emojis.push(root.enabledEmojis[idx])
|
||||
}
|
||||
root.displayedEmojis = emojis
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
updateDisplayedEmojis()
|
||||
}
|
||||
|
||||
onEnabledEmojisChanged: updateDisplayedEmojis()
|
||||
onMaxBarEmojisChanged: updateDisplayedEmojis()
|
||||
|
||||
horizontalBarPill: Component {
|
||||
Row {
|
||||
id: emojiRow
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: root.displayedEmojis
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verticalBarPill: Component {
|
||||
Column {
|
||||
id: emojiColumn
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: root.displayedEmojis
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
popoutContent: Component {
|
||||
PopoutComponent {
|
||||
id: popoutColumn
|
||||
|
||||
headerText: "Emoji Picker"
|
||||
detailsText: "Click an emoji to copy it to clipboard"
|
||||
showCloseButton: true
|
||||
|
||||
property var allEmojis: [
|
||||
"😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃",
|
||||
"😉", "😊", "😇", "🥰", "😍", "🤩", "😘", "😗", "😚", "😙",
|
||||
"😋", "😛", "😜", "🤪", "😝", "🤑", "🤗", "🤭", "🤫", "🤔",
|
||||
"🤐", "🤨", "😐", "😑", "😶", "😏", "😒", "🙄", "😬", "🤥",
|
||||
"😌", "😔", "😪", "🤤", "😴", "😷", "🤒", "🤕", "🤢", "🤮",
|
||||
"🤧", "🥵", "🥶", "😶🌫️", "😵", "😵💫", "🤯", "🤠", "🥳", "😎",
|
||||
"🤓", "🧐", "😕", "😟", "🙁", "☹️", "😮", "😯", "😲", "😳",
|
||||
"🥺", "😦", "😧", "😨", "😰", "😥", "😢", "😭", "😱", "😖",
|
||||
"😣", "😞", "😓", "😩", "😫", "🥱", "😤", "😡", "😠", "🤬",
|
||||
"❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "🤎", "💔",
|
||||
"❤️🔥", "❤️🩹", "💕", "💞", "💓", "💗", "💖", "💘", "💝", "💟",
|
||||
"👍", "👎", "👊", "✊", "🤛", "🤜", "🤞", "✌️", "🤟", "🤘",
|
||||
"👌", "🤌", "🤏", "👈", "👉", "👆", "👇", "☝️", "✋", "🤚"
|
||||
]
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
implicitHeight: root.popoutHeight - popoutColumn.headerHeight - popoutColumn.detailsHeight - Theme.spacingXL
|
||||
|
||||
DankGridView {
|
||||
id: emojiGrid
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: Math.floor(parent.width / 50) * 50
|
||||
height: parent.height
|
||||
clip: true
|
||||
cellWidth: 50
|
||||
cellHeight: 50
|
||||
model: popoutColumn.allEmojis
|
||||
|
||||
delegate: StyledRect {
|
||||
width: 45
|
||||
height: 45
|
||||
radius: Theme.cornerRadius
|
||||
color: emojiMouseArea.containsMouse ? Theme.surfaceContainerHighest : Theme.surfaceContainerHigh
|
||||
border.width: 0
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: emojiMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["sh", "-c", "echo -n '" + modelData + "' | wl-copy"])
|
||||
ToastService.showInfo("Copied " + modelData + " to clipboard")
|
||||
popoutColumn.closePopout()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
popoutWidth: 400
|
||||
popoutHeight: 500
|
||||
}
|
||||
56
PLUGINS/ExampleEmojiPlugin/README.md
Normal file
56
PLUGINS/ExampleEmojiPlugin/README.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Emoji Cycler Plugin
|
||||
|
||||
An example dms plugin that displays cycling emojis in your bar with an emoji picker popout.
|
||||
|
||||
## Features
|
||||
|
||||
- **Cycling Emojis**: Automatically rotates through your selected emoji set in the bar
|
||||
- **Emoji Picker**: Click the widget to open a grid of 120+ emojis
|
||||
- **Copy to Clipboard**: Click any emoji in the picker to copy it to clipboard (uses `wl-copy`)
|
||||
- **Customizable**: Choose emoji sets, cycle speed, and max emojis shown
|
||||
|
||||
## Installation
|
||||
|
||||
1. Copy this directory to `~/.config/DankMaterialShell/plugins/ExampleEmojiPlugin`
|
||||
2. Open DMS Settings → Plugins
|
||||
3. Click "Scan for Plugins"
|
||||
4. Enable "Emoji Cycler"
|
||||
5. Add `exampleEmojiPlugin` to your DankBar widget list
|
||||
|
||||
## Settings
|
||||
|
||||
### Emoji Set
|
||||
Choose from different emoji collections:
|
||||
- **Happy & Sad**: Mix of emotional faces
|
||||
- **Hearts**: Various colored hearts
|
||||
- **Hand Gestures**: Thumbs up, peace signs, etc.
|
||||
- **All Mixed**: A bit of everything
|
||||
|
||||
### Cycle Speed
|
||||
Control how fast emojis rotate (500ms - 10000ms)
|
||||
|
||||
### Max Bar Emojis
|
||||
How many emojis to display at once (1-8)
|
||||
|
||||
## Usage
|
||||
|
||||
**In the bar**: Watch emojis cycle through automatically
|
||||
**Click the widget**: Opens emoji picker with 120+ emojis
|
||||
**Click any emoji**: Copies it to clipboard and shows toast
|
||||
|
||||
## Requirements
|
||||
|
||||
- `wl-copy` (for clipboard support on Wayland)
|
||||
|
||||
## Example Code Highlights
|
||||
|
||||
This plugin demonstrates:
|
||||
- Using `PluginComponent` for bar integration
|
||||
- `SelectionSetting`, `SliderSetting` for configuration
|
||||
- Timer-based animation
|
||||
- Popout content with grid layout
|
||||
- External command execution (`Quickshell.execDetached`)
|
||||
- Toast notifications (`ToastService.show`)
|
||||
- Dynamic settings loading/saving
|
||||
|
||||
Perfect template for creating your own DMS plugins!
|
||||
14
PLUGINS/ExampleEmojiPlugin/plugin.json
Normal file
14
PLUGINS/ExampleEmojiPlugin/plugin.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "exampleEmojiPlugin",
|
||||
"name": "Emoji Cycler",
|
||||
"description": "Display cycling emojis in your bar with a handy emoji picker popout",
|
||||
"version": "1.0.0",
|
||||
"author": "AvengeMedia",
|
||||
"icon": "mood",
|
||||
"component": "./EmojiWidget.qml",
|
||||
"settings": "./EmojiSettings.qml",
|
||||
"permissions": [
|
||||
"settings_read",
|
||||
"settings_write"
|
||||
]
|
||||
}
|
||||
730
PLUGINS/README.md
Normal file
730
PLUGINS/README.md
Normal file
@@ -0,0 +1,730 @@
|
||||
# Plugin System
|
||||
|
||||
The DMS shell includes an experimental plugin system that allows extending functionality through self-contained, dynamically-loaded QML components.
|
||||
|
||||
## Overview
|
||||
|
||||
The plugin system enables developers to create custom widgets that can be displayed in the DankBar alongside built-in widgets. Plugins are discovered, loaded, and managed through the **PluginService**, providing a clean separation between core shell functionality and user extensions.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **PluginService** (`Services/PluginService.qml`)
|
||||
- Singleton service managing plugin lifecycle
|
||||
- Discovers plugins from `$CONFIGPATH/DankMaterialShell/plugins/`
|
||||
- Handles loading, unloading, and state management
|
||||
- Provides data persistence for plugin settings
|
||||
|
||||
2. **PluginsTab** (`Modules/Settings/PluginsTab.qml`)
|
||||
- UI for managing available plugins
|
||||
- Access plugin settings
|
||||
|
||||
3. **PluginsTab Settings** (`Modules/Settings/PluginsTab.qml`)
|
||||
- Accordion-style plugin configuration interface
|
||||
- Dynamically loads plugin settings components inline
|
||||
- Provides consistent settings interface with proper focus handling
|
||||
|
||||
4. **DankBar Integration** (`Modules/DankBar/DankBar.qml`)
|
||||
- Renders plugin widgets in the bar
|
||||
- Merges plugin components with built-in widgets
|
||||
- Supports left, center, and right sections
|
||||
|
||||
## Plugin Structure
|
||||
|
||||
Each plugin must be a directory in `$CONFIGPATH/DankMaterialShell/plugins/` containing:
|
||||
|
||||
```
|
||||
$CONFIGPATH/DankMaterialShell/plugins/YourPlugin/
|
||||
├── plugin.json # Required: Plugin manifest
|
||||
├── YourWidget.qml # Required: Widget component
|
||||
├── YourSettings.qml # Optional: Settings UI
|
||||
├── qmldir # Optional: QML module definition
|
||||
└── *.js # Optional: JavaScript utilities
|
||||
```
|
||||
|
||||
### Plugin Manifest (plugin.json)
|
||||
|
||||
The manifest file defines plugin metadata and configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "yourPlugin",
|
||||
"name": "Your Plugin Name",
|
||||
"description": "Brief description of what your plugin does",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"icon": "material_icon_name",
|
||||
"component": "./YourWidget.qml",
|
||||
"settings": "./YourSettings.qml",
|
||||
"permissions": [
|
||||
"settings_read",
|
||||
"settings_write"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Required Fields:**
|
||||
- `id`: Unique plugin identifier (camelCase, no spaces)
|
||||
- `name`: Human-readable plugin name
|
||||
- `component`: Relative path to widget QML file
|
||||
|
||||
**Optional Fields:**
|
||||
- `description`: Short description of plugin functionality (displayed in UI)
|
||||
- `version`: Semantic version string (displayed in UI)
|
||||
- `author`: Plugin creator name (displayed in UI)
|
||||
- `icon`: Material Design icon name (displayed in UI)
|
||||
- `settings`: Path to settings component (enables settings UI)
|
||||
- `permissions`: Required capabilities (enforced by PluginSettings component)
|
||||
|
||||
**Permissions:**
|
||||
|
||||
The plugin system enforces permissions when settings are accessed:
|
||||
- `settings_read`: Required to read plugin settings (currently not enforced)
|
||||
- `settings_write`: **Required** to use PluginSettings component and save settings
|
||||
|
||||
If your plugin includes a settings component but doesn't declare `settings_write` permission, users will see an error message instead of the settings UI.
|
||||
|
||||
### Widget Component
|
||||
|
||||
The main widget component uses the **PluginComponent** wrapper which provides automatic property injection and bar integration:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
// Define horizontal bar pill (optional)
|
||||
horizontalBarPill: Component {
|
||||
StyledRect {
|
||||
width: content.implicitWidth + Theme.spacingM * 2
|
||||
height: parent.widgetThickness
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
StyledText {
|
||||
id: content
|
||||
anchors.centerIn: parent
|
||||
text: "Hello World"
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define vertical bar pill (optional)
|
||||
verticalBarPill: Component {
|
||||
// Same as horizontal but optimized for vertical layout
|
||||
}
|
||||
|
||||
// Define popout content (optional)
|
||||
popoutContent: Component {
|
||||
PopoutComponent {
|
||||
headerText: "My Plugin"
|
||||
detailsText: "Optional description text goes here"
|
||||
showCloseButton: true
|
||||
|
||||
// Your popout content goes here
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: "Popout Content"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Popout dimensions (required if popoutContent is set)
|
||||
popoutWidth: 400
|
||||
popoutHeight: 300
|
||||
}
|
||||
```
|
||||
|
||||
**PluginComponent Properties (automatically injected):**
|
||||
- `axis`: Bar axis information (horizontal/vertical)
|
||||
- `section`: Bar section ("left", "center", "right")
|
||||
- `parentScreen`: Screen reference for multi-monitor support
|
||||
- `widgetThickness`: Recommended widget size perpendicular to bar
|
||||
- `barThickness`: Bar thickness parallel to edge
|
||||
|
||||
**Component Options:**
|
||||
- `horizontalBarPill`: Component shown in horizontal bars
|
||||
- `verticalBarPill`: Component shown in vertical bars
|
||||
- `popoutContent`: Optional popout window content
|
||||
- `popoutWidth`: Popout window width
|
||||
- `popoutHeight`: Popout window height
|
||||
|
||||
The PluginComponent automatically handles:
|
||||
- Bar orientation detection
|
||||
- Click handlers for popouts
|
||||
- Proper positioning and anchoring
|
||||
- Theme integration
|
||||
|
||||
### PopoutComponent
|
||||
|
||||
PopoutComponent provides a consistent header/content layout for plugin popouts:
|
||||
|
||||
```qml
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PopoutComponent {
|
||||
headerText: "Header Title" // Main header text (bold, large)
|
||||
detailsText: "Description text" // Optional description (smaller, gray)
|
||||
showCloseButton: true // Show X button in top-right
|
||||
|
||||
// Access to exposed properties for dynamic sizing
|
||||
readonly property int headerHeight // Height of header area
|
||||
readonly property int detailsHeight // Height of description area
|
||||
|
||||
// Your content here - use parent.width for full width
|
||||
// Calculate available height: root.popoutHeight - headerHeight - detailsHeight - spacing
|
||||
DankGridView {
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**PopoutComponent Properties:**
|
||||
- `headerText`: Main header text (optional, hidden if empty)
|
||||
- `detailsText`: Description text below header (optional, hidden if empty)
|
||||
- `showCloseButton`: Show close button in header (default: false)
|
||||
- `closePopout`: Function to close popout (auto-injected by PluginPopout)
|
||||
- `headerHeight`: Readonly height of header (0 if not visible)
|
||||
- `detailsHeight`: Readonly height of description (0 if not visible)
|
||||
|
||||
The component automatically handles spacing and layout. Content children are rendered below the description with proper padding.
|
||||
|
||||
### Settings Component
|
||||
|
||||
Optional settings UI loaded inline in the PluginsTab accordion interface. Use the simplified settings API with auto-storage components:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginSettings {
|
||||
id: root
|
||||
pluginId: "yourPlugin"
|
||||
|
||||
StringSetting {
|
||||
settingKey: "apiKey"
|
||||
label: "API Key"
|
||||
description: "Your API key for accessing the service"
|
||||
placeholder: "Enter API key..."
|
||||
}
|
||||
|
||||
ToggleSetting {
|
||||
settingKey: "notifications"
|
||||
label: "Enable Notifications"
|
||||
description: "Show desktop notifications for updates"
|
||||
defaultValue: true
|
||||
}
|
||||
|
||||
SelectionSetting {
|
||||
settingKey: "updateInterval"
|
||||
label: "Update Interval"
|
||||
description: "How often to refresh data"
|
||||
options: [
|
||||
{label: "1 minute", value: "60"},
|
||||
{label: "5 minutes", value: "300"},
|
||||
{label: "15 minutes", value: "900"}
|
||||
]
|
||||
defaultValue: "300"
|
||||
}
|
||||
|
||||
ListSetting {
|
||||
id: itemList
|
||||
settingKey: "items"
|
||||
label: "Saved Items"
|
||||
description: "List of configured items"
|
||||
delegate: Component {
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: modelData.name
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 60
|
||||
height: 28
|
||||
color: removeArea.containsMouse ? Theme.errorHover : Theme.error
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: "Remove"
|
||||
color: Theme.errorText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: removeArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: itemList.removeItem(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available Setting Components:**
|
||||
|
||||
All settings automatically save on change and load on component creation. No manual `pluginService.savePluginData()` calls needed!
|
||||
|
||||
**How Default Values Work:**
|
||||
|
||||
Each setting component has a `defaultValue` property that is used when no saved value exists. Define sensible defaults in your settings UI:
|
||||
|
||||
```qml
|
||||
StringSetting {
|
||||
settingKey: "apiKey"
|
||||
defaultValue: "" // Empty string if no key saved
|
||||
}
|
||||
|
||||
ToggleSetting {
|
||||
settingKey: "enabled"
|
||||
defaultValue: true // Enabled by default
|
||||
}
|
||||
|
||||
ListSettingWithInput {
|
||||
settingKey: "locations"
|
||||
defaultValue: [] // Empty array if no locations saved
|
||||
}
|
||||
```
|
||||
|
||||
1. **PluginSettings** - Root wrapper for all plugin settings
|
||||
- `pluginId`: Your plugin ID (required)
|
||||
- Auto-handles storage and provides saveValue/loadValue to children
|
||||
- Place all other setting components inside this wrapper
|
||||
|
||||
2. **StringSetting** - Text input field
|
||||
- `settingKey`: Storage key (required)
|
||||
- `label`: Display label (required)
|
||||
- `description`: Help text (optional)
|
||||
- `placeholder`: Input placeholder (optional)
|
||||
- `defaultValue`: Default value (optional, default: `""`)
|
||||
- Layout: Vertical stack (label, description, input field)
|
||||
|
||||
3. **ToggleSetting** - Boolean toggle switch
|
||||
- `settingKey`: Storage key (required)
|
||||
- `label`: Display label (required)
|
||||
- `description`: Help text (optional)
|
||||
- `defaultValue`: Default boolean (optional, default: `false`)
|
||||
- Layout: Horizontal (label/description left, toggle right)
|
||||
|
||||
4. **SelectionSetting** - Dropdown menu
|
||||
- `settingKey`: Storage key (required)
|
||||
- `label`: Display label (required)
|
||||
- `description`: Help text (optional)
|
||||
- `options`: Array of `{label, value}` objects or simple strings (required)
|
||||
- `defaultValue`: Default value (optional, default: `""`)
|
||||
- Layout: Horizontal (label/description left, dropdown right)
|
||||
- Stores the `value` field, displays the `label` field
|
||||
|
||||
5. **ListSetting** - Manage list of items (manual add/remove)
|
||||
- `settingKey`: Storage key (required)
|
||||
- `label`: Display label (required)
|
||||
- `description`: Help text (optional)
|
||||
- `defaultValue`: Default array (optional, default: `[]`)
|
||||
- `delegate`: Custom item delegate Component (optional)
|
||||
- `addItem(item)`: Add item to list
|
||||
- `removeItem(index)`: Remove item from list
|
||||
- Use when you need custom UI for adding items
|
||||
|
||||
6. **ListSettingWithInput** - Complete list management with built-in form
|
||||
- `settingKey`: Storage key (required)
|
||||
- `label`: Display label (required)
|
||||
- `description`: Help text (optional)
|
||||
- `defaultValue`: Default array (optional, default: `[]`)
|
||||
- `fields`: Array of field definitions (required)
|
||||
- `id`: Field ID in saved object (required)
|
||||
- `label`: Column header text (required)
|
||||
- `placeholder`: Input placeholder (optional)
|
||||
- `width`: Column width in pixels (optional, default 200)
|
||||
- `required`: Must have value to add (optional, default false)
|
||||
- `default`: Default value if empty (optional)
|
||||
- Automatically generates:
|
||||
- Column headers from field labels
|
||||
- Input fields with placeholders
|
||||
- Add button with validation
|
||||
- List display showing all field values
|
||||
- Remove buttons for each item
|
||||
- Best for collecting structured data (servers, locations, etc.)
|
||||
|
||||
**Complete Settings Example:**
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginSettings {
|
||||
pluginId: "myPlugin"
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: "General Settings"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StringSetting {
|
||||
settingKey: "apiKey"
|
||||
label: "API Key"
|
||||
description: "Your service API key"
|
||||
placeholder: "sk-..."
|
||||
defaultValue: ""
|
||||
}
|
||||
|
||||
ToggleSetting {
|
||||
settingKey: "enabled"
|
||||
label: "Enable Feature"
|
||||
description: "Turn this feature on or off"
|
||||
defaultValue: true
|
||||
}
|
||||
|
||||
SelectionSetting {
|
||||
settingKey: "theme"
|
||||
label: "Theme"
|
||||
description: "Choose your preferred theme"
|
||||
options: [
|
||||
{label: "Dark", value: "dark"},
|
||||
{label: "Light", value: "light"},
|
||||
{label: "Auto", value: "auto"}
|
||||
]
|
||||
defaultValue: "dark"
|
||||
}
|
||||
|
||||
ListSettingWithInput {
|
||||
settingKey: "locations"
|
||||
label: "Locations"
|
||||
description: "Track multiple locations"
|
||||
defaultValue: []
|
||||
fields: [
|
||||
{id: "name", label: "Name", placeholder: "Home", width: 150, required: true},
|
||||
{id: "timezone", label: "Timezone", placeholder: "America/New_York", width: 200, required: true}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Benefits:**
|
||||
- Zero boilerplate - just define your settings
|
||||
- Automatic persistence to `settings.json`
|
||||
- Clean, consistent UI across all plugins
|
||||
- No manual `pluginService` calls needed
|
||||
- Proper layout and spacing handled automatically
|
||||
|
||||
## PluginService API
|
||||
|
||||
### Properties
|
||||
|
||||
```qml
|
||||
PluginService.pluginDirectory: string
|
||||
// Path to plugins directory ($CONFIGPATH/DankMaterialShell/plugins)
|
||||
|
||||
PluginService.availablePlugins: object
|
||||
// Map of all discovered plugins {pluginId: pluginInfo}
|
||||
|
||||
PluginService.loadedPlugins: object
|
||||
// Map of currently loaded plugins {pluginId: pluginInfo}
|
||||
|
||||
PluginService.pluginWidgetComponents: object
|
||||
// Map of loaded widget components {pluginId: Component}
|
||||
```
|
||||
|
||||
### Functions
|
||||
|
||||
```qml
|
||||
// Plugin Management
|
||||
PluginService.loadPlugin(pluginId: string): bool
|
||||
PluginService.unloadPlugin(pluginId: string): bool
|
||||
PluginService.reloadPlugin(pluginId: string): bool
|
||||
PluginService.enablePlugin(pluginId: string): bool
|
||||
PluginService.disablePlugin(pluginId: string): bool
|
||||
|
||||
// Plugin Discovery
|
||||
PluginService.scanPlugins(): void
|
||||
PluginService.getAvailablePlugins(): array
|
||||
PluginService.getLoadedPlugins(): array
|
||||
PluginService.isPluginLoaded(pluginId: string): bool
|
||||
PluginService.getWidgetComponents(): object
|
||||
|
||||
// Data Persistence
|
||||
PluginService.savePluginData(pluginId: string, key: string, value: any): bool
|
||||
PluginService.loadPluginData(pluginId: string, key: string, defaultValue: any): any
|
||||
```
|
||||
|
||||
### Signals
|
||||
|
||||
```qml
|
||||
PluginService.pluginLoaded(pluginId: string)
|
||||
PluginService.pluginUnloaded(pluginId: string)
|
||||
PluginService.pluginLoadFailed(pluginId: string, error: string)
|
||||
```
|
||||
|
||||
## Creating a Plugin
|
||||
|
||||
### Step 1: Create Plugin Directory
|
||||
|
||||
```bash
|
||||
mkdir -p $CONFIGPATH/DankMaterialShell/plugins/MyPlugin
|
||||
cd $CONFIGPATH/DankMaterialShell/plugins/MyPlugin
|
||||
```
|
||||
|
||||
### Step 2: Create Manifest
|
||||
|
||||
Create `plugin.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "myPlugin",
|
||||
"name": "My Plugin",
|
||||
"description": "A sample plugin",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"icon": "extension",
|
||||
"component": "./MyWidget.qml",
|
||||
"settings": "./MySettings.qml",
|
||||
"permissions": ["settings_read", "settings_write"]
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Create Widget Component
|
||||
|
||||
Create `MyWidget.qml`:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
horizontalBarPill: Component {
|
||||
StyledRect {
|
||||
width: textItem.implicitWidth + Theme.spacingM * 2
|
||||
height: parent.widgetThickness
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
StyledText {
|
||||
id: textItem
|
||||
anchors.centerIn: parent
|
||||
text: "Hello World"
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verticalBarPill: Component {
|
||||
StyledRect {
|
||||
width: parent.widgetThickness
|
||||
height: textItem.implicitWidth + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
StyledText {
|
||||
id: textItem
|
||||
anchors.centerIn: parent
|
||||
text: "Hello"
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
rotation: 90
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Use `PluginComponent` wrapper for automatic property injection and bar integration. Define separate components for horizontal and vertical orientations.
|
||||
|
||||
### Step 4: Create Settings Component (Optional)
|
||||
|
||||
Create `MySettings.qml`:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginSettings {
|
||||
pluginId: "myPlugin"
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: "Configure your plugin settings"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
StringSetting {
|
||||
settingKey: "text"
|
||||
label: "Display Text"
|
||||
description: "Text shown in the bar widget"
|
||||
placeholder: "Hello World"
|
||||
defaultValue: "Hello World"
|
||||
}
|
||||
|
||||
ToggleSetting {
|
||||
settingKey: "showIcon"
|
||||
label: "Show Icon"
|
||||
description: "Display an icon next to the text"
|
||||
defaultValue: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Enable Plugin
|
||||
|
||||
1. Run the shell: `qs -p $CONFIGPATH/quickshell/dms/shell.qml`
|
||||
2. Open Settings (Ctrl+,)
|
||||
3. Navigate to Plugins tab
|
||||
4. Click "Scan for Plugins"
|
||||
5. Enable your plugin with the toggle switch
|
||||
6. Add the plugin to your DankBar configuration
|
||||
|
||||
## Adding Plugin to DankBar
|
||||
|
||||
After enabling a plugin, add it to the bar:
|
||||
|
||||
1. Open Settings → Appearance → DankBar Layout
|
||||
2. Add a new widget entry with your plugin ID
|
||||
3. Choose section (left, center, right)
|
||||
4. Save and reload
|
||||
|
||||
Or edit `$CONFIGPATH/quickshell/dms/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"dankBarLeftWidgets": [
|
||||
{"widgetId": "myPlugin", "enabled": true}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Existing Widgets**: Leverage `qs.Widgets` components (DankIcon, DankToggle, etc.) for consistency
|
||||
2. **Follow Theme**: Use `Theme` singleton for colors, spacing, and fonts
|
||||
3. **Data Persistence**: Use PluginService data APIs instead of manual file operations
|
||||
4. **Error Handling**: Gracefully handle missing dependencies and invalid data
|
||||
5. **Performance**: Keep widgets lightweight, avoid expensive operations
|
||||
6. **Responsive Design**: Adapt to `compactMode` and different screen sizes
|
||||
7. **Clean Code**: Follow QML code conventions from CLAUDE.md
|
||||
8. **Documentation**: Include README.md explaining plugin usage
|
||||
9. **Versioning**: Use semantic versioning for updates
|
||||
10. **Dependencies**: Document external library requirements
|
||||
|
||||
## Debugging
|
||||
|
||||
### Console Logging
|
||||
|
||||
View plugin logs:
|
||||
|
||||
```bash
|
||||
qs -v -p $CONFIGPATH/quickshell/dms/shell.qml
|
||||
```
|
||||
|
||||
Look for lines prefixed with:
|
||||
- `PluginService:` - Service operations
|
||||
- `PluginsTab:` - UI interactions
|
||||
- `PluginsTab:` - Settings loading and accordion interface
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Plugin Not Detected**
|
||||
- Check plugin.json syntax (use `jq` or JSON validator)
|
||||
- Verify directory is in `$CONFIGPATH/DankMaterialShell/plugins/`
|
||||
- Click "Scan for Plugins" in Settings
|
||||
|
||||
2. **Widget Not Displaying**
|
||||
- Ensure plugin is enabled in Settings
|
||||
- Add plugin ID to DankBar widget list
|
||||
- Check widget width/height properties
|
||||
|
||||
3. **Settings Not Loading**
|
||||
- Verify `settings` path in plugin.json
|
||||
- Check settings component for errors
|
||||
- Ensure plugin is enabled and loaded
|
||||
- Review PluginsTab console output for injection issues
|
||||
|
||||
4. **Data Not Persisting**
|
||||
- Confirm pluginService.savePluginData() calls (with injection)
|
||||
- Check `$CONFIGPATH/DankMaterialShell/settings.json` for pluginSettings data
|
||||
- Verify plugin has settings permissions
|
||||
- Ensure PluginService was properly injected into settings component
|
||||
|
||||
## Security Considerations
|
||||
|
||||
Plugins run with full QML runtime access. Only install plugins from trusted sources.
|
||||
|
||||
**Permissions System:**
|
||||
- `settings_read`: Read plugin configuration (not currently enforced)
|
||||
- `settings_write`: **Required** to use PluginSettings - write plugin configuration (enforced)
|
||||
- `process`: Execute system commands (not currently enforced)
|
||||
- `network`: Network access (not currently enforced)
|
||||
|
||||
Currently, only `settings_write` is enforced by the PluginSettings component.
|
||||
|
||||
## API Stability
|
||||
|
||||
The plugin API is currently **experimental**. Breaking changes may occur in minor version updates. Pin to specific DMS versions for production use.
|
||||
|
||||
**Roadmap:**
|
||||
- Plugin marketplace/repository
|
||||
- Sandboxed plugin execution
|
||||
- Enhanced permission system
|
||||
- Plugin update notifications
|
||||
- Inter-plugin communication
|
||||
|
||||
## Resources
|
||||
|
||||
- **Example Plugin**: https://github.com/rochacbruno/WorldClock
|
||||
- **PluginService**: `Services/PluginService.qml`
|
||||
- **Settings UI**: `Modules/Settings/PluginsTab.qml`
|
||||
- **DankBar Integration**: `Modules/DankBar/DankBar.qml`
|
||||
- **Theme Reference**: `Common/Theme.qml`
|
||||
- **Widget Library**: `Widgets/`
|
||||
|
||||
## Contributing
|
||||
|
||||
Share your plugins with the community:
|
||||
|
||||
1. Create a public repository with your plugin
|
||||
2. Include comprehensive README.md
|
||||
4. Add example screenshots
|
||||
5. Document dependencies and permissions
|
||||
|
||||
For plugin system improvements, submit issues or PRs to the main DMS repository.
|
||||
35
README.md
35
README.md
@@ -11,14 +11,14 @@
|
||||
|
||||
</div>
|
||||
|
||||
A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/) and designed for the [niri](https://github.com/YaLTeR/niri) and [Hyprland](https://hyprland.org/) compositors. Features Material 3 design principles with a heavy focus on functionality and customizability.
|
||||
A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/) and optimized for the [niri](https://github.com/YaLTeR/niri) and [Hyprland](https://hyprland.org/) compositors.
|
||||
|
||||
## Screenshots
|
||||
|
||||
<div align="center">
|
||||
<div style="max-width: 700px; margin: 0 auto;">
|
||||
|
||||
https://github.com/user-attachments/assets/fd619c0e-6edc-457e-b3d6-5a5c3bae7173
|
||||
https://github.com/user-attachments/assets/9b99dbbf-42d3-44ab-83b6-fae6c2aa3cc0
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,7 +43,7 @@ https://github.com/user-attachments/assets/fd619c0e-6edc-457e-b3d6-5a5c3bae7173
|
||||
|
||||
### Control Center
|
||||
|
||||
<img width="600" alt="Control Center" src="https://github.com/user-attachments/assets/98889bd8-55d2-44c7-b278-75ca49c596fa" />
|
||||
<img width="600" alt="Control Center" src="https://github.com/user-attachments/assets/732c30de-5f4a-4a2b-a995-c8ab656cecd5" />
|
||||
|
||||
### System Monitor
|
||||
|
||||
@@ -122,6 +122,8 @@ curl -fsSL https://install.danklinux.com | sh
|
||||
- Configure bluetooth, wifi, and audio input+output devices.
|
||||
- A lock screen
|
||||
- Idle monitoring - configure auto lock, screen off, suspend, and hibernate with different knobs for battery + AC power.
|
||||
- A greeter
|
||||
- A comprehensive plugin system for endless customization possibilities.
|
||||
|
||||
**TL;DR** *dms replaces your waybar, swaylock, swayidle, hypridle, hyprlock, fuzzels, walker, mako, and basically everything you use to stitch a desktop together*
|
||||
|
||||
@@ -313,7 +315,7 @@ sudo sh -c "curl -L https://github.com/AvengeMedia/dgop/releases/latest/download
|
||||
|
||||
A lot of options are subject to personal preference, but the below sets a good starting point for most features.
|
||||
|
||||
### Niri Integration
|
||||
### niri Integration
|
||||
|
||||
Add to your niri config
|
||||
|
||||
@@ -393,6 +395,17 @@ binds {
|
||||
}
|
||||
```
|
||||
|
||||
#### niri theming
|
||||
|
||||
If using a niri build newer than [3933903](https://github.com/YaLTeR/niri/commit/39339032cee3453faa54c361a38db6d83756f750), you can synchronize colors and gaps with the shell settings by adding the following to your niri config.
|
||||
|
||||
```bash
|
||||
# For colors
|
||||
echo -e 'include "dms/colors.kdl"' >> ~/.config/niri/config.kdl
|
||||
# For gaps, border widths, certain window rules
|
||||
echo -e 'include "dms/layout.kdl"' >> ~/.config/niri/config.kdl
|
||||
```
|
||||
|
||||
### Hyprland Integration
|
||||
|
||||
Add to your Hyprland config (`~/.config/hypr/hyprland.conf`):
|
||||
@@ -440,6 +453,12 @@ bindl = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
|
||||
bind = SUPERSHIFT, N, exec, dms ipc call night toggle
|
||||
```
|
||||
|
||||
## Greeter
|
||||
|
||||
You can install a matching [greetd](https://github.com/kennylevinsen/greetd) greeter, that will give you a greeter that matches the lock screen.
|
||||
|
||||
It's as simple as running `dms greeter install` in most cases, but more information is in the [Greetd module](Modules/Greetd/README.md)
|
||||
|
||||
## IPC Commands
|
||||
|
||||
Control everything from the command line, or via keybinds. For comprehensive documentation of all available IPC commands, see [docs/IPC.md](docs/IPC.md).
|
||||
@@ -623,6 +642,14 @@ echo "app-notifications = no-clipboard-copy,no-config-reload" >> ~/.config/ghost
|
||||
echo "include dank-theme.conf" >> ~/.config/kitty/kitty.conf
|
||||
```
|
||||
|
||||
## Plugins
|
||||
|
||||
dms features a plugin system - meaning you can create your own widgets and load other user widgets.
|
||||
|
||||
More comprehensive details available in the [PLUGINS](PLUGINS/README.md) - and example [Emoji Plugin](PLUGINS/ExampleEmojiPlugin) is available for reference.
|
||||
|
||||
The example plugin can be installed by `cp -R ./PLUGINS/ExampleEmojiPlugin ~/.config/DankMaterialShell/plugins` - then it will appear in dms settings.
|
||||
|
||||
### Calendar Setup
|
||||
|
||||
Sync your caldev compatible calendar (Google, Office365, etc.) for dashboard integration:
|
||||
|
||||
@@ -82,6 +82,7 @@ Singleton {
|
||||
|
||||
Component.onCompleted: {
|
||||
detectCompositor()
|
||||
NiriService.generateNiriLayoutConfig()
|
||||
}
|
||||
|
||||
function filterCurrentWorkspace(toplevels, screen) {
|
||||
@@ -192,6 +193,7 @@ Singleton {
|
||||
root.isHyprland = false
|
||||
root.compositor = "niri"
|
||||
console.log("CompositorService: Detected Niri with socket:", root.niriSocket)
|
||||
NiriService.generateNiriBinds()
|
||||
} else {
|
||||
root.isHyprland = false
|
||||
root.isNiri = true
|
||||
@@ -200,4 +202,4 @@ Singleton {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -708,7 +708,6 @@ Singleton {
|
||||
|
||||
onExited: function (exitCode) {
|
||||
geoclueAvailable = (exitCode === 0)
|
||||
console.log("DisplayService: geoclue available:", geoclueAvailable)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -31,6 +33,7 @@ Singleton {
|
||||
property bool suppressConfigToast: true
|
||||
property bool suppressNextConfigToast: false
|
||||
property bool matugenSuppression: false
|
||||
property bool configGenerationPending: false
|
||||
|
||||
readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
|
||||
|
||||
@@ -412,13 +415,13 @@ Singleton {
|
||||
}
|
||||
|
||||
function doScreenTransition() {
|
||||
send({
|
||||
"Action": {
|
||||
"DoScreenTransition": {
|
||||
"delay_ms": 0,
|
||||
return send({
|
||||
"Action": {
|
||||
"DoScreenTransition": {
|
||||
"delay_ms": 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function switchToWorkspace(workspaceIndex) {
|
||||
@@ -652,6 +655,7 @@ Singleton {
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
Timer {
|
||||
id: suppressToastTimer
|
||||
interval: 3000
|
||||
@@ -663,4 +667,101 @@ Singleton {
|
||||
interval: 2000
|
||||
onTriggered: root.matugenSuppression = false
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: configGenerationDebounce
|
||||
interval: 100
|
||||
onTriggered: root.doGenerateNiriLayoutConfig()
|
||||
}
|
||||
|
||||
function generateNiriLayoutConfig() {
|
||||
const niriSocket = Quickshell.env("NIRI_SOCKET")
|
||||
if (!niriSocket || niriSocket.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (configGenerationPending) {
|
||||
return
|
||||
}
|
||||
|
||||
configGenerationPending = true
|
||||
configGenerationDebounce.restart()
|
||||
}
|
||||
|
||||
function doGenerateNiriLayoutConfig() {
|
||||
console.log("NiriService: Generating layout config...")
|
||||
|
||||
const cornerRadius = typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12
|
||||
const gaps = typeof SettingsData !== "undefined" ? Math.max(4, SettingsData.dankBarSpacing) : 4
|
||||
|
||||
const configContent = `layout {
|
||||
gaps ${gaps}
|
||||
|
||||
border {
|
||||
width 2
|
||||
}
|
||||
|
||||
focus-ring {
|
||||
width 2
|
||||
}
|
||||
}
|
||||
|
||||
window-rule {
|
||||
geometry-corner-radius ${cornerRadius}
|
||||
clip-to-geometry true
|
||||
tiled-state true
|
||||
draw-border-with-background false
|
||||
}`
|
||||
|
||||
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
|
||||
const niriDmsDir = configDir + "/niri/dms"
|
||||
const configPath = niriDmsDir + "/layout.kdl"
|
||||
|
||||
writeConfigProcess.configContent = configContent
|
||||
writeConfigProcess.configPath = configPath
|
||||
writeConfigProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && cat > "${configPath}" << 'EOF'\n${configContent}\nEOF`]
|
||||
writeConfigProcess.running = true
|
||||
configGenerationPending = false
|
||||
}
|
||||
|
||||
function generateNiriBinds() {
|
||||
console.log("NiriService: Generating binds config...")
|
||||
|
||||
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
|
||||
const niriDmsDir = configDir + "/niri/dms"
|
||||
const bindsPath = niriDmsDir + "/binds.kdl"
|
||||
const sourceBindsPath = Paths.strip(Qt.resolvedUrl("niri-binds.kdl"))
|
||||
|
||||
writeBindsProcess.bindsPath = bindsPath
|
||||
writeBindsProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && cp "${sourceBindsPath}" "${bindsPath}"`]
|
||||
writeBindsProcess.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: writeConfigProcess
|
||||
property string configContent: ""
|
||||
property string configPath: ""
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
console.log("NiriService: Generated layout config at", configPath)
|
||||
} else {
|
||||
console.warn("NiriService: Failed to write layout config, exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: writeBindsProcess
|
||||
property string bindsPath: ""
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
console.log("NiriService: Generated binds config at", bindsPath)
|
||||
} else {
|
||||
console.warn("NiriService: Failed to write binds config, exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
326
Services/PluginService.qml
Normal file
326
Services/PluginService.qml
Normal file
@@ -0,0 +1,326 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property var availablePlugins: ({})
|
||||
property var loadedPlugins: ({})
|
||||
property var pluginWidgetComponents: ({})
|
||||
property string pluginDirectory: {
|
||||
var configDir = StandardPaths.writableLocation(StandardPaths.ConfigLocation)
|
||||
var configDirStr = configDir.toString()
|
||||
if (configDirStr.startsWith("file://")) {
|
||||
configDirStr = configDirStr.substring(7)
|
||||
}
|
||||
return configDirStr + "/DankMaterialShell/plugins"
|
||||
}
|
||||
property string systemPluginDirectory: "/etc/xdg/quickshell/dms-plugins"
|
||||
property var pluginDirectories: [pluginDirectory, systemPluginDirectory]
|
||||
|
||||
signal pluginLoaded(string pluginId)
|
||||
signal pluginUnloaded(string pluginId)
|
||||
signal pluginLoadFailed(string pluginId, string error)
|
||||
signal pluginDataChanged(string pluginId)
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(initializePlugins)
|
||||
}
|
||||
|
||||
function initializePlugins() {
|
||||
scanPlugins()
|
||||
}
|
||||
|
||||
property int currentScanIndex: 0
|
||||
property var scanResults: []
|
||||
|
||||
property var lsProcess: Process {
|
||||
id: dirScanner
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var output = text.trim()
|
||||
var currentDir = pluginDirectories[currentScanIndex]
|
||||
if (output) {
|
||||
var directories = output.split('\n')
|
||||
for (var i = 0; i < directories.length; i++) {
|
||||
var dir = directories[i].trim()
|
||||
if (dir) {
|
||||
var manifestPath = currentDir + "/" + dir + "/plugin.json"
|
||||
loadPluginManifest(manifestPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function(exitCode) {
|
||||
currentScanIndex++
|
||||
if (currentScanIndex < pluginDirectories.length) {
|
||||
scanNextDirectory()
|
||||
} else {
|
||||
currentScanIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scanPlugins() {
|
||||
currentScanIndex = 0
|
||||
scanNextDirectory()
|
||||
}
|
||||
|
||||
function scanNextDirectory() {
|
||||
var dir = pluginDirectories[currentScanIndex]
|
||||
lsProcess.command = ["find", "-L", dir, "-maxdepth", "1", "-type", "d", "-not", "-path", dir, "-exec", "basename", "{}", ";"]
|
||||
lsProcess.running = true
|
||||
}
|
||||
|
||||
property var manifestReaders: ({})
|
||||
|
||||
function loadPluginManifest(manifestPath) {
|
||||
var readerId = "reader_" + Date.now() + "_" + Math.random()
|
||||
|
||||
var catProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { stdout: StdioCollector { } }")
|
||||
if (catProcess.status === Component.Ready) {
|
||||
var process = catProcess.createObject(root)
|
||||
process.command = ["cat", manifestPath]
|
||||
process.stdout.streamFinished.connect(function() {
|
||||
try {
|
||||
var manifest = JSON.parse(process.stdout.text.trim())
|
||||
processManifest(manifest, manifestPath)
|
||||
} catch (e) {
|
||||
console.error("PluginService: Failed to parse manifest", manifestPath, ":", e.message)
|
||||
}
|
||||
process.destroy()
|
||||
delete manifestReaders[readerId]
|
||||
})
|
||||
process.exited.connect(function(exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
console.error("PluginService: Failed to read manifest file:", manifestPath, "exit code:", exitCode)
|
||||
process.destroy()
|
||||
delete manifestReaders[readerId]
|
||||
}
|
||||
})
|
||||
manifestReaders[readerId] = process
|
||||
process.running = true
|
||||
} else {
|
||||
console.error("PluginService: Failed to create manifest reader process")
|
||||
}
|
||||
}
|
||||
|
||||
function processManifest(manifest, manifestPath) {
|
||||
registerPlugin(manifest, manifestPath)
|
||||
|
||||
// Auto-load plugin if it's enabled in settings (default to enabled)
|
||||
var enabled = SettingsData.getPluginSetting(manifest.id, "enabled", true)
|
||||
if (enabled) {
|
||||
loadPlugin(manifest.id)
|
||||
}
|
||||
}
|
||||
|
||||
function registerPlugin(manifest, manifestPath) {
|
||||
if (!manifest.id || !manifest.name || !manifest.component) {
|
||||
console.error("PluginService: Invalid manifest, missing required fields:", manifestPath)
|
||||
return
|
||||
}
|
||||
|
||||
var pluginDir = manifestPath.substring(0, manifestPath.lastIndexOf('/'))
|
||||
|
||||
// Clean up relative paths by removing './' prefix
|
||||
var componentFile = manifest.component
|
||||
if (componentFile.startsWith('./')) {
|
||||
componentFile = componentFile.substring(2)
|
||||
}
|
||||
|
||||
var settingsFile = manifest.settings
|
||||
if (settingsFile && settingsFile.startsWith('./')) {
|
||||
settingsFile = settingsFile.substring(2)
|
||||
}
|
||||
|
||||
var pluginInfo = {}
|
||||
for (var key in manifest) {
|
||||
pluginInfo[key] = manifest[key]
|
||||
}
|
||||
pluginInfo.manifestPath = manifestPath
|
||||
pluginInfo.pluginDirectory = pluginDir
|
||||
pluginInfo.componentPath = pluginDir + '/' + componentFile
|
||||
pluginInfo.settingsPath = settingsFile ? pluginDir + '/' + settingsFile : null
|
||||
pluginInfo.loaded = false
|
||||
|
||||
availablePlugins[manifest.id] = pluginInfo
|
||||
}
|
||||
|
||||
function hasPermission(pluginId, permission) {
|
||||
var plugin = availablePlugins[pluginId]
|
||||
if (!plugin) {
|
||||
return false
|
||||
}
|
||||
var permissions = plugin.permissions || []
|
||||
return permissions.indexOf(permission) !== -1
|
||||
}
|
||||
|
||||
function loadPlugin(pluginId) {
|
||||
var plugin = availablePlugins[pluginId]
|
||||
if (!plugin) {
|
||||
console.error("PluginService: Plugin not found:", pluginId)
|
||||
pluginLoadFailed(pluginId, "Plugin not found")
|
||||
return false
|
||||
}
|
||||
|
||||
if (plugin.loaded) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (pluginWidgetComponents[pluginId]) {
|
||||
var oldComponent = pluginWidgetComponents[pluginId]
|
||||
if (oldComponent) {
|
||||
oldComponent.destroy()
|
||||
}
|
||||
delete pluginWidgetComponents[pluginId]
|
||||
}
|
||||
|
||||
try {
|
||||
var componentUrl = "file://" + plugin.componentPath
|
||||
var component = Qt.createComponent(componentUrl, Component.PreferSynchronous)
|
||||
|
||||
if (component.status === Component.Loading) {
|
||||
component.statusChanged.connect(function() {
|
||||
if (component.status === Component.Error) {
|
||||
console.error("PluginService: Failed to create component for plugin:", pluginId, "Error:", component.errorString())
|
||||
pluginLoadFailed(pluginId, component.errorString())
|
||||
component.destroy()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (component.status === Component.Error) {
|
||||
console.error("PluginService: Failed to create component for plugin:", pluginId, "Error:", component.errorString())
|
||||
pluginLoadFailed(pluginId, component.errorString())
|
||||
component.destroy()
|
||||
return false
|
||||
}
|
||||
|
||||
var newComponents = Object.assign({}, pluginWidgetComponents)
|
||||
newComponents[pluginId] = component
|
||||
pluginWidgetComponents = newComponents
|
||||
|
||||
plugin.loaded = true
|
||||
loadedPlugins[pluginId] = plugin
|
||||
|
||||
pluginLoaded(pluginId)
|
||||
return true
|
||||
|
||||
} catch (error) {
|
||||
console.error("PluginService: Error loading plugin:", pluginId, "Error:", error.message)
|
||||
pluginLoadFailed(pluginId, error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function unloadPlugin(pluginId) {
|
||||
var plugin = loadedPlugins[pluginId]
|
||||
if (!plugin) {
|
||||
console.warn("PluginService: Plugin not loaded:", pluginId)
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
if (pluginWidgetComponents[pluginId]) {
|
||||
var component = pluginWidgetComponents[pluginId]
|
||||
if (component) {
|
||||
component.destroy()
|
||||
}
|
||||
}
|
||||
var newComponents = Object.assign({}, pluginWidgetComponents)
|
||||
delete newComponents[pluginId]
|
||||
pluginWidgetComponents = newComponents
|
||||
|
||||
plugin.loaded = false
|
||||
delete loadedPlugins[pluginId]
|
||||
|
||||
pluginUnloaded(pluginId)
|
||||
return true
|
||||
|
||||
} catch (error) {
|
||||
console.error("PluginService: Error unloading plugin:", pluginId, "Error:", error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function getWidgetComponents() {
|
||||
return pluginWidgetComponents
|
||||
}
|
||||
|
||||
function getAvailablePlugins() {
|
||||
var result = []
|
||||
for (var key in availablePlugins) {
|
||||
result.push(availablePlugins[key])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function getLoadedPlugins() {
|
||||
var result = []
|
||||
for (var key in loadedPlugins) {
|
||||
result.push(loadedPlugins[key])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function isPluginLoaded(pluginId) {
|
||||
return loadedPlugins[pluginId] !== undefined
|
||||
}
|
||||
|
||||
function enablePlugin(pluginId) {
|
||||
SettingsData.setPluginSetting(pluginId, "enabled", true)
|
||||
return loadPlugin(pluginId)
|
||||
}
|
||||
|
||||
function disablePlugin(pluginId) {
|
||||
SettingsData.setPluginSetting(pluginId, "enabled", false)
|
||||
return unloadPlugin(pluginId)
|
||||
}
|
||||
|
||||
function reloadPlugin(pluginId) {
|
||||
if (isPluginLoaded(pluginId)) {
|
||||
unloadPlugin(pluginId)
|
||||
}
|
||||
return loadPlugin(pluginId)
|
||||
}
|
||||
|
||||
function savePluginData(pluginId, key, value) {
|
||||
SettingsData.setPluginSetting(pluginId, key, value)
|
||||
pluginDataChanged(pluginId)
|
||||
return true
|
||||
}
|
||||
|
||||
function loadPluginData(pluginId, key, defaultValue) {
|
||||
return SettingsData.getPluginSetting(pluginId, key, defaultValue)
|
||||
}
|
||||
|
||||
function createPluginDirectory() {
|
||||
var mkdirProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { }")
|
||||
if (mkdirProcess.status === Component.Ready) {
|
||||
var process = mkdirProcess.createObject(root)
|
||||
process.command = ["mkdir", "-p", pluginDirectory]
|
||||
process.exited.connect(function(exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
console.error("PluginService: Failed to create plugin directory, exit code:", exitCode)
|
||||
}
|
||||
process.destroy()
|
||||
})
|
||||
process.running = true
|
||||
return true
|
||||
} else {
|
||||
console.error("PluginService: Failed to create mkdir process")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,42 @@ Singleton {
|
||||
systemProfileCheckProcess.running = true
|
||||
}
|
||||
|
||||
function getUserProfileImage(username) {
|
||||
if (!username) {
|
||||
profileImage = ""
|
||||
return
|
||||
}
|
||||
if (Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true") {
|
||||
profileImage = ""
|
||||
return
|
||||
}
|
||||
userProfileCheckProcess.command = [
|
||||
"bash", "-c",
|
||||
`uid=$(id -u ${username} 2>/dev/null) && [ -n "$uid" ] && dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$uid org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile 2>/dev/null | grep -oP 'string "\\K[^"]+' || echo ""`
|
||||
]
|
||||
userProfileCheckProcess.running = true
|
||||
}
|
||||
|
||||
function getGreeterUserProfileImage(username) {
|
||||
if (!username) {
|
||||
profileImage = ""
|
||||
return
|
||||
}
|
||||
userProfileCheckProcess.command = [
|
||||
"bash", "-c",
|
||||
`uid=$(id -u ${username} 2>/dev/null) && [ -n "$uid" ] && dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$uid org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile 2>/dev/null | grep -oP 'string "\\K[^"]+' || echo ""`
|
||||
]
|
||||
userProfileCheckProcess.running = true
|
||||
}
|
||||
|
||||
function setProfileImage(imagePath) {
|
||||
profileImage = imagePath
|
||||
if (accountsServiceAvailable && imagePath) {
|
||||
setSystemProfileImage(imagePath)
|
||||
if (accountsServiceAvailable) {
|
||||
if (imagePath) {
|
||||
setSystemProfileImage(imagePath)
|
||||
} else {
|
||||
setSystemProfileImage("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,11 +83,12 @@ Singleton {
|
||||
}
|
||||
|
||||
function setSystemProfileImage(imagePath) {
|
||||
if (!accountsServiceAvailable || !imagePath) {
|
||||
if (!accountsServiceAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
const script = `dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$(id -u) org.freedesktop.Accounts.User.SetIconFile string:'${imagePath}'`
|
||||
const path = imagePath || ""
|
||||
const script = `dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$(id -u) org.freedesktop.Accounts.User.SetIconFile string:'${path}'`
|
||||
|
||||
systemProfileSetProcess.command = ["bash", "-c", script]
|
||||
systemProfileSetProcess.running = true
|
||||
@@ -123,6 +156,29 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: userProfileCheckProcess
|
||||
command: []
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const trimmed = text.trim()
|
||||
if (trimmed && trimmed !== "" && !trimmed.includes("Error") && trimmed !== "/var/lib/AccountsService/icons/") {
|
||||
root.profileImage = trimmed
|
||||
} else {
|
||||
root.profileImage = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
root.profileImage = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: settingsPortalCheckProcess
|
||||
command: ["gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop", "--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne", "org.freedesktop.appearance", "color-scheme"]
|
||||
|
||||
55
Services/niri-binds.kdl
Normal file
55
Services/niri-binds.kdl
Normal file
@@ -0,0 +1,55 @@
|
||||
binds {
|
||||
Mod+Space hotkey-overlay-title="Application Launcher" {
|
||||
spawn "dms" "ipc" "call" "spotlight" "toggle";
|
||||
}
|
||||
|
||||
Mod+V hotkey-overlay-title="Clipboard Manager" {
|
||||
spawn "dms" "ipc" "call" "clipboard" "toggle";
|
||||
}
|
||||
|
||||
Mod+M hotkey-overlay-title="Task Manager" {
|
||||
spawn "dms" "ipc" "call" "processlist" "toggle";
|
||||
}
|
||||
|
||||
Mod+Comma hotkey-overlay-title="Settings" {
|
||||
spawn "dms" "ipc" "call" "settings" "toggle";
|
||||
}
|
||||
|
||||
Mod+N hotkey-overlay-title="Notification Center" {
|
||||
spawn "dms" "ipc" "call" "notifications" "toggle";
|
||||
}
|
||||
|
||||
Mod+Shift+N hotkey-overlay-title="Notepad" {
|
||||
spawn "dms" "ipc" "call" "notepad" "toggle";
|
||||
}
|
||||
|
||||
Mod+Alt+L hotkey-overlay-title="Lock Screen" {
|
||||
spawn "dms" "ipc" "call" "lock" "lock";
|
||||
}
|
||||
|
||||
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
|
||||
spawn "dms" "ipc" "call" "processlist" "toggle";
|
||||
}
|
||||
|
||||
// Audio
|
||||
XF86AudioRaiseVolume allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "audio" "increment" "3";
|
||||
}
|
||||
XF86AudioLowerVolume allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "audio" "decrement" "3";
|
||||
}
|
||||
XF86AudioMute allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "audio" "mute";
|
||||
}
|
||||
XF86AudioMicMute allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "audio" "micmute";
|
||||
}
|
||||
|
||||
// BL
|
||||
XF86MonBrightnessUp allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "brightness" "increment" "5" "";
|
||||
}
|
||||
XF86MonBrightnessDown allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "brightness" "decrement" "5" "";
|
||||
}
|
||||
}
|
||||
75
Widgets/DankButton.qml
Normal file
75
Widgets/DankButton.qml
Normal file
@@ -0,0 +1,75 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string text: ""
|
||||
property string iconName: ""
|
||||
property int iconSize: Theme.iconSizeSmall
|
||||
property bool enabled: true
|
||||
property bool hovered: mouseArea.containsMouse
|
||||
property bool pressed: mouseArea.pressed
|
||||
property color backgroundColor: Theme.primary
|
||||
property color textColor: Theme.primaryText
|
||||
property int buttonHeight: 40
|
||||
property int horizontalPadding: Theme.spacingL
|
||||
|
||||
signal clicked()
|
||||
|
||||
width: Math.max(contentRow.implicitWidth + horizontalPadding * 2, 64)
|
||||
height: buttonHeight
|
||||
radius: Theme.cornerRadius
|
||||
color: backgroundColor
|
||||
opacity: enabled ? 1 : 0.4
|
||||
|
||||
Rectangle {
|
||||
id: stateLayer
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: {
|
||||
if (pressed) return Theme.primaryPressed
|
||||
if (hovered) return Theme.primaryHover
|
||||
return "transparent"
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shorterDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: contentRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: root.iconName
|
||||
size: root.iconSize
|
||||
color: root.textColor
|
||||
visible: root.iconName !== ""
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.text
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: root.textColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: root.enabled
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ Rectangle {
|
||||
name: root.fallbackIcon
|
||||
size: parent.width * 0.5
|
||||
color: Theme.surfaceVariantText
|
||||
visible: internalImage.status !== Image.Ready && root.imageSource === "" && root.fallbackIcon !== ""
|
||||
visible: (internalImage.status !== Image.Ready || root.imageSource === "") && root.fallbackIcon !== ""
|
||||
}
|
||||
|
||||
|
||||
@@ -77,8 +77,8 @@ Rectangle {
|
||||
anchors.centerIn: parent
|
||||
visible: root.imageSource === "" && root.fallbackIcon === "" && root.fallbackText !== ""
|
||||
text: root.fallbackText
|
||||
font.pixelSize: Math.max(12, parent.width * 0.36)
|
||||
font.pixelSize: Math.max(12, parent.width * 0.5)
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primaryText
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import "../Common/fzf.js" as Fzf
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string text: ""
|
||||
@@ -12,47 +13,33 @@ Rectangle {
|
||||
property string currentValue: ""
|
||||
property var options: []
|
||||
property var optionIcons: []
|
||||
property bool forceRecreate: false
|
||||
property bool enableFuzzySearch: false
|
||||
property int popupWidthOffset: 0
|
||||
property int maxPopupHeight: 400
|
||||
property bool openUpwards: false
|
||||
property int popupWidth: 0
|
||||
property bool alignPopupRight: false
|
||||
property int dropdownWidth: 200
|
||||
|
||||
signal valueChanged(string value)
|
||||
|
||||
width: parent.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
Component.onCompleted: forceRecreateTimer.start()
|
||||
implicitHeight: Math.max(60, labelColumn.implicitHeight + Theme.spacingM)
|
||||
|
||||
Component.onDestruction: {
|
||||
const popup = popupLoader.item
|
||||
const popup = dropdownMenu
|
||||
if (popup && popup.visible) {
|
||||
popup.close()
|
||||
}
|
||||
}
|
||||
onVisibleChanged: {
|
||||
const popup = popupLoader.item
|
||||
if (!visible && popup && popup.visible) {
|
||||
popup.close()
|
||||
} else if (visible) {
|
||||
forceRecreateTimer.start()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: forceRecreateTimer
|
||||
|
||||
interval: 50
|
||||
repeat: false
|
||||
onTriggered: root.forceRecreate = !root.forceRecreate
|
||||
}
|
||||
|
||||
Column {
|
||||
id: labelColumn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: dropdown.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
@@ -75,15 +62,14 @@ Rectangle {
|
||||
Rectangle {
|
||||
id: dropdown
|
||||
|
||||
width: root.width <= 60 ? root.width : 180
|
||||
height: 36
|
||||
width: root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : root.dropdownWidth)
|
||||
height: 40
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: Theme.cornerRadius
|
||||
color: dropdownArea.containsMouse ? Theme.primaryHover : Theme.contentBackground()
|
||||
border.color: Theme.surfaceVariantAlpha
|
||||
border.width: 1
|
||||
color: dropdownArea.containsMouse || dropdownMenu.visible ? Theme.surfaceContainerHigh : Theme.surfaceContainer
|
||||
border.color: dropdownMenu.visible ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: dropdownMenu.visible ? 2 : 1
|
||||
|
||||
MouseArea {
|
||||
id: dropdownArea
|
||||
@@ -92,20 +78,40 @@ Rectangle {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
const popup = popupLoader.item
|
||||
if (!popup) {
|
||||
if (dropdownMenu.visible) {
|
||||
dropdownMenu.close()
|
||||
return
|
||||
}
|
||||
|
||||
if (popup.visible) {
|
||||
popup.close()
|
||||
return
|
||||
dropdownMenu.searchQuery = ""
|
||||
dropdownMenu.updateFilteredOptions()
|
||||
|
||||
dropdownMenu.open()
|
||||
|
||||
const pos = dropdown.mapToItem(Overlay.overlay, 0, 0)
|
||||
const popupWidth = dropdownMenu.width
|
||||
const popupHeight = dropdownMenu.height
|
||||
const overlayHeight = Overlay.overlay.height
|
||||
|
||||
if (root.openUpwards || pos.y + dropdown.height + popupHeight + 4 > overlayHeight) {
|
||||
if (root.alignPopupRight) {
|
||||
dropdownMenu.x = pos.x + dropdown.width - popupWidth
|
||||
} else {
|
||||
dropdownMenu.x = pos.x - (root.popupWidthOffset / 2)
|
||||
}
|
||||
dropdownMenu.y = pos.y - popupHeight - 4
|
||||
} else {
|
||||
if (root.alignPopupRight) {
|
||||
dropdownMenu.x = pos.x + dropdown.width - popupWidth
|
||||
} else {
|
||||
dropdownMenu.x = pos.x - (root.popupWidthOffset / 2)
|
||||
}
|
||||
dropdownMenu.y = pos.y + dropdown.height + 4
|
||||
}
|
||||
|
||||
const pos = dropdown.mapToItem(Overlay.overlay, 0, dropdown.height + 4)
|
||||
popup.x = pos.x - (root.popupWidthOffset / 2)
|
||||
popup.y = pos.y
|
||||
popup.open()
|
||||
if (root.enableFuzzySearch && searchField.visible) {
|
||||
searchField.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,8 +119,10 @@ Rectangle {
|
||||
id: contentRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: expandIcon.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
@@ -123,9 +131,9 @@ Rectangle {
|
||||
return currentIndex >= 0 && root.optionIcons.length > currentIndex ? root.optionIcons[currentIndex] : ""
|
||||
}
|
||||
size: 18
|
||||
color: Theme.surfaceVariantText
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: name !== "" && root.width > 60
|
||||
visible: name !== ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
@@ -133,228 +141,220 @@ Rectangle {
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: root.width <= 60 ? dropdown.width - expandIcon.width - Theme.spacingS * 2 : dropdown.width - contentRow.x - expandIcon.width - Theme.spacingM - Theme.spacingS
|
||||
elide: root.width <= 60 ? Text.ElideNone : Text.ElideRight
|
||||
horizontalAlignment: root.width <= 60 ? Text.AlignHCenter : Text.AlignLeft
|
||||
width: contentRow.width - (contentRow.children[0].visible ? contentRow.children[0].width + contentRow.spacing : 0)
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: expandIcon
|
||||
|
||||
name: "expand_more"
|
||||
name: dropdownMenu.visible ? "expand_less" : "expand_more"
|
||||
size: 20
|
||||
color: Theme.surfaceVariantText
|
||||
color: Theme.surfaceText
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
|
||||
Behavior on rotation {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: popupLoader
|
||||
Popup {
|
||||
id: dropdownMenu
|
||||
|
||||
property bool recreateFlag: root.forceRecreate
|
||||
property string searchQuery: ""
|
||||
property var filteredOptions: []
|
||||
property int selectedIndex: -1
|
||||
property var fzfFinder: new Fzf.Finder(root.options, {
|
||||
"selector": option => option,
|
||||
"limit": 50,
|
||||
"casing": "case-insensitive"
|
||||
})
|
||||
|
||||
active: true
|
||||
onRecreateFlagChanged: {
|
||||
active = false
|
||||
active = true
|
||||
function updateFilteredOptions() {
|
||||
if (!root.enableFuzzySearch || searchQuery.length === 0) {
|
||||
filteredOptions = root.options
|
||||
selectedIndex = -1
|
||||
return
|
||||
}
|
||||
|
||||
const results = fzfFinder.find(searchQuery)
|
||||
filteredOptions = results.map(result => result.item)
|
||||
selectedIndex = -1
|
||||
}
|
||||
|
||||
sourceComponent: Component {
|
||||
Popup {
|
||||
id: dropdownMenu
|
||||
function selectNext() {
|
||||
if (filteredOptions.length === 0) {
|
||||
return
|
||||
}
|
||||
selectedIndex = (selectedIndex + 1) % filteredOptions.length
|
||||
listView.positionViewAtIndex(selectedIndex, ListView.Contain)
|
||||
}
|
||||
|
||||
property string searchQuery: ""
|
||||
property var filteredOptions: []
|
||||
property int selectedIndex: -1
|
||||
property var fzfFinder: new Fzf.Finder(root.options, {
|
||||
"selector": option => option,
|
||||
"limit": 50,
|
||||
"casing": "case-insensitive"
|
||||
})
|
||||
function selectPrevious() {
|
||||
if (filteredOptions.length === 0) {
|
||||
return
|
||||
}
|
||||
selectedIndex = selectedIndex <= 0 ? filteredOptions.length - 1 : selectedIndex - 1
|
||||
listView.positionViewAtIndex(selectedIndex, ListView.Contain)
|
||||
}
|
||||
|
||||
function updateFilteredOptions() {
|
||||
if (!root.enableFuzzySearch || searchQuery.length === 0) {
|
||||
filteredOptions = root.options
|
||||
selectedIndex = -1
|
||||
return
|
||||
}
|
||||
function selectCurrent() {
|
||||
if (selectedIndex < 0 || selectedIndex >= filteredOptions.length) {
|
||||
return
|
||||
}
|
||||
root.currentValue = filteredOptions[selectedIndex]
|
||||
root.valueChanged(filteredOptions[selectedIndex])
|
||||
close()
|
||||
}
|
||||
|
||||
const results = fzfFinder.find(searchQuery)
|
||||
filteredOptions = results.map(result => result.item)
|
||||
selectedIndex = -1
|
||||
}
|
||||
parent: Overlay.overlay
|
||||
width: root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : (dropdown.width + root.popupWidthOffset))
|
||||
height: Math.min(root.maxPopupHeight, (root.enableFuzzySearch ? 54 : 0) + Math.min(filteredOptions.length, 10) * 36 + 16)
|
||||
padding: 0
|
||||
modal: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
function selectNext() {
|
||||
if (filteredOptions.length === 0) {
|
||||
return
|
||||
}
|
||||
selectedIndex = (selectedIndex + 1) % filteredOptions.length
|
||||
listView.positionViewAtIndex(selectedIndex, ListView.Contain)
|
||||
}
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (filteredOptions.length === 0) {
|
||||
return
|
||||
}
|
||||
selectedIndex = selectedIndex <= 0 ? filteredOptions.length - 1 : selectedIndex - 1
|
||||
listView.positionViewAtIndex(selectedIndex, ListView.Contain)
|
||||
}
|
||||
contentItem: Rectangle {
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1)
|
||||
border.color: Theme.primary
|
||||
border.width: 2
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
function selectCurrent() {
|
||||
if (selectedIndex < 0 || selectedIndex >= filteredOptions.length) {
|
||||
return
|
||||
}
|
||||
root.currentValue = filteredOptions[selectedIndex]
|
||||
root.valueChanged(filteredOptions[selectedIndex])
|
||||
close()
|
||||
}
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowBlur: 0.4
|
||||
shadowColor: Theme.shadowStrong
|
||||
shadowVerticalOffset: 4
|
||||
}
|
||||
|
||||
parent: Overlay.overlay
|
||||
width: dropdown.width + root.popupWidthOffset
|
||||
height: Math.min(root.maxPopupHeight, (root.enableFuzzySearch ? 54 : 0) + Math.min(filteredOptions.length, 10) * 36 + 16)
|
||||
padding: 0
|
||||
modal: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
onOpened: {
|
||||
searchQuery = ""
|
||||
updateFilteredOptions()
|
||||
if (root.enableFuzzySearch && searchField.visible) {
|
||||
searchField.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
Rectangle {
|
||||
id: searchContainer
|
||||
|
||||
contentItem: Rectangle {
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1)
|
||||
border.color: Theme.primarySelected
|
||||
border.width: 1
|
||||
width: parent.width
|
||||
height: 42
|
||||
visible: root.enableFuzzySearch
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
DankTextField {
|
||||
id: searchField
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
anchors.margins: 1
|
||||
placeholderText: "Search..."
|
||||
text: dropdownMenu.searchQuery
|
||||
topPadding: Theme.spacingS
|
||||
bottomPadding: Theme.spacingS
|
||||
onTextChanged: {
|
||||
dropdownMenu.searchQuery = text
|
||||
dropdownMenu.updateFilteredOptions()
|
||||
}
|
||||
Keys.onDownPressed: dropdownMenu.selectNext()
|
||||
Keys.onUpPressed: dropdownMenu.selectPrevious()
|
||||
Keys.onReturnPressed: dropdownMenu.selectCurrent()
|
||||
Keys.onEnterPressed: dropdownMenu.selectCurrent()
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
|
||||
dropdownMenu.selectNext()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
|
||||
dropdownMenu.selectPrevious()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
|
||||
dropdownMenu.selectNext()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
|
||||
dropdownMenu.selectPrevious()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: searchContainer
|
||||
Item {
|
||||
width: 1
|
||||
height: Theme.spacingXS
|
||||
visible: root.enableFuzzySearch
|
||||
}
|
||||
|
||||
width: parent.width
|
||||
height: 42
|
||||
visible: root.enableFuzzySearch
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceVariantAlpha
|
||||
DankListView {
|
||||
id: listView
|
||||
|
||||
DankTextField {
|
||||
id: searchField
|
||||
width: parent.width
|
||||
height: parent.height - (root.enableFuzzySearch ? searchContainer.height + Theme.spacingXS : 0)
|
||||
clip: true
|
||||
model: dropdownMenu.filteredOptions
|
||||
spacing: 2
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
placeholderText: "Search..."
|
||||
text: searchQuery
|
||||
topPadding: Theme.spacingS
|
||||
bottomPadding: Theme.spacingS
|
||||
onTextChanged: {
|
||||
searchQuery = text
|
||||
updateFilteredOptions()
|
||||
}
|
||||
Keys.onDownPressed: selectNext()
|
||||
Keys.onUpPressed: selectPrevious()
|
||||
Keys.onReturnPressed: selectCurrent()
|
||||
Keys.onEnterPressed: selectCurrent()
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
|
||||
selectNext()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
|
||||
selectPrevious()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
|
||||
selectNext()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
|
||||
selectPrevious()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
interactive: true
|
||||
flickDeceleration: 1500
|
||||
maximumFlickVelocity: 2000
|
||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
||||
boundsMovement: Flickable.FollowBoundsBehavior
|
||||
pressDelay: 0
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
delegate: Rectangle {
|
||||
property bool isSelected: dropdownMenu.selectedIndex === index
|
||||
property bool isCurrentValue: root.currentValue === modelData
|
||||
property int optionIndex: root.options.indexOf(modelData)
|
||||
|
||||
width: ListView.view.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: isSelected ? Theme.primaryHover : optionArea.containsMouse ? Theme.primaryHoverLight : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: optionIndex >= 0 && root.optionIcons.length > optionIndex ? root.optionIcons[optionIndex] : ""
|
||||
size: 18
|
||||
color: isCurrentValue ? Theme.primary : Theme.surfaceText
|
||||
visible: name !== ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: isCurrentValue ? Theme.primary : Theme.surfaceText
|
||||
font.weight: isCurrentValue ? Font.Medium : Font.Normal
|
||||
width: root.popupWidth > 0 ? undefined : (parent.parent.width - parent.x - Theme.spacingS)
|
||||
elide: root.popupWidth > 0 ? Text.ElideNone : Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: Theme.spacingXS
|
||||
visible: root.enableFuzzySearch
|
||||
}
|
||||
MouseArea {
|
||||
id: optionArea
|
||||
|
||||
DankListView {
|
||||
id: listView
|
||||
|
||||
property var popupRef: dropdownMenu
|
||||
|
||||
width: parent.width
|
||||
height: parent.height - (root.enableFuzzySearch ? searchContainer.height + Theme.spacingXS : 0)
|
||||
clip: true
|
||||
model: filteredOptions
|
||||
spacing: 2
|
||||
|
||||
interactive: true
|
||||
flickDeceleration: 1500
|
||||
maximumFlickVelocity: 2000
|
||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
||||
boundsMovement: Flickable.FollowBoundsBehavior
|
||||
pressDelay: 0
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
delegate: Rectangle {
|
||||
property bool isSelected: selectedIndex === index
|
||||
property bool isCurrentValue: root.currentValue === modelData
|
||||
property int optionIndex: root.options.indexOf(modelData)
|
||||
|
||||
width: ListView.view.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: isSelected ? Theme.primaryHover : optionArea.containsMouse ? Theme.primaryHoverLight : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: optionIndex >= 0 && root.optionIcons.length > optionIndex ? root.optionIcons[optionIndex] : ""
|
||||
size: 18
|
||||
color: isCurrentValue ? Theme.primary : Theme.surfaceVariantText
|
||||
visible: name !== ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: isCurrentValue ? Theme.primary : Theme.surfaceText
|
||||
font.weight: isCurrentValue ? Font.Medium : Font.Normal
|
||||
width: parent.parent.width - parent.x - Theme.spacingS
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: optionArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.currentValue = modelData
|
||||
root.valueChanged(modelData)
|
||||
listView.popupRef.close()
|
||||
}
|
||||
}
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.currentValue = modelData
|
||||
root.valueChanged(modelData)
|
||||
dropdownMenu.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ PanelWindow {
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
layer.enabled: false
|
||||
layer.enabled: true
|
||||
|
||||
readonly property real screenWidth: root.screen ? root.screen.width : 1920
|
||||
readonly property real screenHeight: root.screen ? root.screen.height : 1080
|
||||
|
||||
@@ -44,7 +44,7 @@ Item {
|
||||
DankIcon {
|
||||
name: slider.leftIcon
|
||||
size: Theme.iconSize
|
||||
color: slider.enabled ? Theme.onSurface : Theme.onSurface_38
|
||||
color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: slider.leftIcon.length > 0
|
||||
}
|
||||
@@ -265,7 +265,7 @@ Item {
|
||||
DankIcon {
|
||||
name: slider.rightIcon
|
||||
size: Theme.iconSize
|
||||
color: slider.enabled ? Theme.onSurface : Theme.onSurface_38
|
||||
color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: slider.rightIcon.length > 0
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[templates.niri]
|
||||
input_path = './matugen/templates/niri-colors.kdl'
|
||||
output_path = '~/.config/niri/dankshell-colors.kdl'
|
||||
output_path = '~/.config/niri/dms/colors.kdl'
|
||||
633
shell.qml
633
shell.qml
@@ -2,642 +2,23 @@
|
||||
//@ pragma UseQApplication
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Modals
|
||||
import qs.Modals.Clipboard
|
||||
import qs.Modals.Common
|
||||
import qs.Modals.Settings
|
||||
import qs.Modals.Spotlight
|
||||
import qs.Modules
|
||||
import qs.Modules.AppDrawer
|
||||
import qs.Modules.DankDash
|
||||
import qs.Modules.ControlCenter
|
||||
import qs.Modules.Dock
|
||||
import qs.Modules.Lock
|
||||
import qs.Modules.Notifications.Center
|
||||
import qs.Widgets
|
||||
import "./Modules/Notepad"
|
||||
import qs.Modules.Notifications.Popup
|
||||
import qs.Modules.OSD
|
||||
import qs.Modules.ProcessList
|
||||
import qs.Modules.Settings
|
||||
import qs.Modules.DankBar
|
||||
import qs.Modules.DankBar.Popouts
|
||||
import qs.Services
|
||||
|
||||
ShellRoot {
|
||||
id: root
|
||||
|
||||
Component.onCompleted: {
|
||||
PortalService.init()
|
||||
// Initialize DisplayService night mode functionality
|
||||
DisplayService.nightModeEnabled
|
||||
// Initialize WallpaperCyclingService
|
||||
WallpaperCyclingService.cyclingActive
|
||||
}
|
||||
|
||||
WallpaperBackground {}
|
||||
|
||||
Lock {
|
||||
id: lock
|
||||
|
||||
anchors.fill: parent
|
||||
}
|
||||
readonly property bool runGreeter: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
|
||||
Loader {
|
||||
id: dankBarLoader
|
||||
id: dmsShellLoader
|
||||
asynchronous: false
|
||||
|
||||
property var currentPosition: SettingsData.dankBarPosition
|
||||
|
||||
sourceComponent: DankBar {
|
||||
onColorPickerRequested: colorPickerModal.show()
|
||||
}
|
||||
|
||||
onCurrentPositionChanged: {
|
||||
const component = sourceComponent
|
||||
sourceComponent = null
|
||||
Qt.callLater(() => {
|
||||
sourceComponent = component
|
||||
})
|
||||
}
|
||||
sourceComponent: DMSShell{}
|
||||
active: !root.runGreeter
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: dockLoader
|
||||
active: true
|
||||
id: dmsGreeterLoader
|
||||
asynchronous: false
|
||||
|
||||
property var currentPosition: SettingsData.dockPosition
|
||||
|
||||
sourceComponent: Dock {
|
||||
contextMenu: dockContextMenuLoader.item ? dockContextMenuLoader.item : null
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
dockContextMenuLoader.active = true
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentPositionChanged: {
|
||||
console.log("DEBUG: Dock position changed to:", currentPosition, "- recreating dock")
|
||||
const comp = sourceComponent
|
||||
sourceComponent = null
|
||||
Qt.callLater(() => {
|
||||
sourceComponent = comp
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: dankDashPopoutLoader
|
||||
|
||||
active: false
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: Component {
|
||||
DankDashPopout {
|
||||
id: dankDashPopout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: dockContextMenuLoader
|
||||
|
||||
active: false
|
||||
|
||||
DockContextMenu {
|
||||
id: dockContextMenu
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: notificationCenterLoader
|
||||
|
||||
active: false
|
||||
|
||||
NotificationCenterPopout {
|
||||
id: notificationCenter
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("notifications")
|
||||
|
||||
delegate: NotificationPopupManager {
|
||||
modelData: item
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: controlCenterLoader
|
||||
|
||||
active: false
|
||||
|
||||
property var modalRef: colorPickerModal
|
||||
|
||||
ControlCenterPopout {
|
||||
id: controlCenterPopout
|
||||
colorPickerModal: controlCenterLoader.modalRef
|
||||
|
||||
onPowerActionRequested: (action, title, message) => {
|
||||
powerConfirmModalLoader.active = true
|
||||
if (powerConfirmModalLoader.item) {
|
||||
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
|
||||
powerConfirmModalLoader.item.show(title, message, function () {
|
||||
switch (action) {
|
||||
case "logout":
|
||||
SessionService.logout()
|
||||
break
|
||||
case "suspend":
|
||||
SessionService.suspend()
|
||||
break
|
||||
case "hibernate":
|
||||
SessionService.hibernate()
|
||||
break
|
||||
case "reboot":
|
||||
SessionService.reboot()
|
||||
break
|
||||
case "poweroff":
|
||||
SessionService.poweroff()
|
||||
break
|
||||
}
|
||||
}, function () {})
|
||||
}
|
||||
}
|
||||
onLockRequested: {
|
||||
lock.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: wifiPasswordModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
WifiPasswordModal {
|
||||
id: wifiPasswordModal
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: networkInfoModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
NetworkInfoModal {
|
||||
id: networkInfoModal
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: batteryPopoutLoader
|
||||
|
||||
active: false
|
||||
|
||||
BatteryPopout {
|
||||
id: batteryPopout
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: vpnPopoutLoader
|
||||
|
||||
active: false
|
||||
|
||||
VpnPopout {
|
||||
id: vpnPopout
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: powerMenuLoader
|
||||
|
||||
active: false
|
||||
|
||||
PowerMenu {
|
||||
id: powerMenu
|
||||
|
||||
onPowerActionRequested: (action, title, message) => {
|
||||
powerConfirmModalLoader.active = true
|
||||
if (powerConfirmModalLoader.item) {
|
||||
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
|
||||
powerConfirmModalLoader.item.show(title, message, function () {
|
||||
switch (action) {
|
||||
case "logout":
|
||||
SessionService.logout()
|
||||
break
|
||||
case "suspend":
|
||||
SessionService.suspend()
|
||||
break
|
||||
case "hibernate":
|
||||
SessionService.hibernate()
|
||||
break
|
||||
case "reboot":
|
||||
SessionService.reboot()
|
||||
break
|
||||
case "poweroff":
|
||||
SessionService.poweroff()
|
||||
break
|
||||
}
|
||||
}, function () {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: powerConfirmModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
ConfirmModal {
|
||||
id: powerConfirmModal
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: processListPopoutLoader
|
||||
|
||||
active: false
|
||||
|
||||
ProcessListPopout {
|
||||
id: processListPopout
|
||||
}
|
||||
}
|
||||
|
||||
SettingsModal {
|
||||
id: settingsModal
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: appDrawerLoader
|
||||
|
||||
active: false
|
||||
|
||||
AppDrawerPopout {
|
||||
id: appDrawerPopout
|
||||
}
|
||||
}
|
||||
|
||||
SpotlightModal {
|
||||
id: spotlightModal
|
||||
}
|
||||
|
||||
ClipboardHistoryModal {
|
||||
id: clipboardHistoryModalPopup
|
||||
}
|
||||
|
||||
NotificationModal {
|
||||
id: notificationModal
|
||||
}
|
||||
ColorPickerModal {
|
||||
id: colorPickerModal
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: processListModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
ProcessListModal {
|
||||
id: processListModal
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: systemUpdateLoader
|
||||
|
||||
active: false
|
||||
|
||||
SystemUpdatePopout {
|
||||
id: systemUpdatePopout
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
id: notepadSlideoutVariants
|
||||
model: SettingsData.getFilteredScreens("notepad")
|
||||
|
||||
delegate: DankSlideout {
|
||||
id: notepadSlideout
|
||||
modelData: item
|
||||
title: qsTr("Notepad")
|
||||
slideoutWidth: 480
|
||||
expandable: true
|
||||
expandedWidthValue: 960
|
||||
customTransparency: SettingsData.notepadTransparencyOverride
|
||||
|
||||
content: Component {
|
||||
Notepad {
|
||||
onHideRequested: {
|
||||
notepadSlideout.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (isVisible) {
|
||||
hide()
|
||||
} else {
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: powerMenuModalLoader
|
||||
|
||||
active: false
|
||||
|
||||
PowerMenuModal {
|
||||
id: powerMenuModal
|
||||
|
||||
onPowerActionRequested: (action, title, message) => {
|
||||
powerConfirmModalLoader.active = true
|
||||
if (powerConfirmModalLoader.item) {
|
||||
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
|
||||
powerConfirmModalLoader.item.show(title, message, function () {
|
||||
switch (action) {
|
||||
case "logout":
|
||||
SessionService.logout()
|
||||
break
|
||||
case "suspend":
|
||||
SessionService.suspend()
|
||||
break
|
||||
case "hibernate":
|
||||
SessionService.hibernate()
|
||||
break
|
||||
case "reboot":
|
||||
SessionService.reboot()
|
||||
break
|
||||
case "poweroff":
|
||||
SessionService.poweroff()
|
||||
break
|
||||
}
|
||||
}, function () {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open() {
|
||||
powerMenuModalLoader.active = true
|
||||
if (powerMenuModalLoader.item)
|
||||
powerMenuModalLoader.item.open()
|
||||
|
||||
return "POWERMENU_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (powerMenuModalLoader.item)
|
||||
powerMenuModalLoader.item.close()
|
||||
|
||||
return "POWERMENU_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
powerMenuModalLoader.active = true
|
||||
if (powerMenuModalLoader.item)
|
||||
powerMenuModalLoader.item.toggle()
|
||||
|
||||
return "POWERMENU_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
target: "powermenu"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
processListModalLoader.active = true
|
||||
if (processListModalLoader.item)
|
||||
processListModalLoader.item.show()
|
||||
|
||||
return "PROCESSLIST_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (processListModalLoader.item)
|
||||
processListModalLoader.item.hide()
|
||||
|
||||
return "PROCESSLIST_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
processListModalLoader.active = true
|
||||
if (processListModalLoader.item)
|
||||
processListModalLoader.item.toggle()
|
||||
|
||||
return "PROCESSLIST_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
target: "processlist"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
controlCenterLoader.active = true
|
||||
if (controlCenterLoader.item) {
|
||||
controlCenterLoader.item.open()
|
||||
return "CONTROL_CENTER_OPEN_SUCCESS"
|
||||
}
|
||||
return "CONTROL_CENTER_OPEN_FAILED"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (controlCenterLoader.item) {
|
||||
controlCenterLoader.item.close()
|
||||
return "CONTROL_CENTER_CLOSE_SUCCESS"
|
||||
}
|
||||
return "CONTROL_CENTER_CLOSE_FAILED"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
controlCenterLoader.active = true
|
||||
if (controlCenterLoader.item) {
|
||||
controlCenterLoader.item.toggle()
|
||||
return "CONTROL_CENTER_TOGGLE_SUCCESS"
|
||||
}
|
||||
return "CONTROL_CENTER_TOGGLE_FAILED"
|
||||
}
|
||||
|
||||
target: "control-center"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(tab: string): string {
|
||||
dankDashPopoutLoader.active = true
|
||||
if (dankDashPopoutLoader.item) {
|
||||
switch (tab.toLowerCase()) {
|
||||
case "media":
|
||||
dankDashPopoutLoader.item.currentTabIndex = 1
|
||||
break
|
||||
case "weather":
|
||||
dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0
|
||||
break
|
||||
default:
|
||||
dankDashPopoutLoader.item.currentTabIndex = 0
|
||||
break
|
||||
}
|
||||
dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen)
|
||||
dankDashPopoutLoader.item.dashVisible = true
|
||||
return "DASH_OPEN_SUCCESS"
|
||||
}
|
||||
return "DASH_OPEN_FAILED"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (dankDashPopoutLoader.item) {
|
||||
dankDashPopoutLoader.item.dashVisible = false
|
||||
return "DASH_CLOSE_SUCCESS"
|
||||
}
|
||||
return "DASH_CLOSE_FAILED"
|
||||
}
|
||||
|
||||
function toggle(tab: string): string {
|
||||
dankDashPopoutLoader.active = true
|
||||
if (dankDashPopoutLoader.item) {
|
||||
if (dankDashPopoutLoader.item.dashVisible) {
|
||||
dankDashPopoutLoader.item.dashVisible = false
|
||||
} else {
|
||||
switch (tab.toLowerCase()) {
|
||||
case "media":
|
||||
dankDashPopoutLoader.item.currentTabIndex = 1
|
||||
break
|
||||
case "weather":
|
||||
dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0
|
||||
break
|
||||
default:
|
||||
dankDashPopoutLoader.item.currentTabIndex = 0
|
||||
break
|
||||
}
|
||||
dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen)
|
||||
dankDashPopoutLoader.item.dashVisible = true
|
||||
}
|
||||
return "DASH_TOGGLE_SUCCESS"
|
||||
}
|
||||
return "DASH_TOGGLE_FAILED"
|
||||
}
|
||||
|
||||
target: "dash"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function getFocusedScreenName() {
|
||||
if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) {
|
||||
return Hyprland.focusedWorkspace.monitor.name
|
||||
}
|
||||
if (CompositorService.isNiri && NiriService.currentOutput) {
|
||||
return NiriService.currentOutput
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function getActiveNotepadInstance() {
|
||||
if (notepadSlideoutVariants.instances.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (notepadSlideoutVariants.instances.length === 1) {
|
||||
return notepadSlideoutVariants.instances[0]
|
||||
}
|
||||
|
||||
var focusedScreen = getFocusedScreenName()
|
||||
if (focusedScreen && notepadSlideoutVariants.instances.length > 0) {
|
||||
for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) {
|
||||
var slideout = notepadSlideoutVariants.instances[i]
|
||||
if (slideout.modelData && slideout.modelData.name === focusedScreen) {
|
||||
return slideout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) {
|
||||
var slideout = notepadSlideoutVariants.instances[i]
|
||||
if (slideout.isVisible) {
|
||||
return slideout
|
||||
}
|
||||
}
|
||||
|
||||
return notepadSlideoutVariants.instances[0]
|
||||
}
|
||||
|
||||
function open(): string {
|
||||
var instance = getActiveNotepadInstance()
|
||||
if (instance) {
|
||||
instance.show()
|
||||
return "NOTEPAD_OPEN_SUCCESS"
|
||||
}
|
||||
return "NOTEPAD_OPEN_FAILED"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
var instance = getActiveNotepadInstance()
|
||||
if (instance) {
|
||||
instance.hide()
|
||||
return "NOTEPAD_CLOSE_SUCCESS"
|
||||
}
|
||||
return "NOTEPAD_CLOSE_FAILED"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
var instance = getActiveNotepadInstance()
|
||||
if (instance) {
|
||||
instance.toggle()
|
||||
return "NOTEPAD_TOGGLE_SUCCESS"
|
||||
}
|
||||
return "NOTEPAD_TOGGLE_FAILED"
|
||||
}
|
||||
|
||||
target: "notepad"
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("toast")
|
||||
|
||||
delegate: Toast {
|
||||
modelData: item
|
||||
visible: ToastService.toastVisible
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("osd")
|
||||
|
||||
delegate: VolumeOSD {
|
||||
modelData: item
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("osd")
|
||||
|
||||
delegate: MicMuteOSD {
|
||||
modelData: item
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("osd")
|
||||
|
||||
delegate: BrightnessOSD {
|
||||
modelData: item
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.getFilteredScreens("osd")
|
||||
|
||||
delegate: IdleInhibitorOSD {
|
||||
modelData: item
|
||||
}
|
||||
sourceComponent: DMSGreeter{}
|
||||
active: root.runGreeter
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user