mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-06 05:25:41 -05:00
Compare commits
9 Commits
dms-niri
...
wip/plugin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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"
|
||||
blockLoading: true
|
||||
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
|
||||
blockLoading: isGreeterMode
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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,36 @@ 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) {
|
||||
console.log("CenterSection: Injecting PluginService into plugin widget:", model.widgetId)
|
||||
item.pluginService = PluginService
|
||||
if (item.loadTimezones) {
|
||||
console.log("CenterSection: Calling loadTimezones for widget:", model.widgetId)
|
||||
item.loadTimezones()
|
||||
}
|
||||
}
|
||||
|
||||
layoutTimer.restart()
|
||||
}
|
||||
|
||||
@@ -379,4 +420,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,24 @@ Item {
|
||||
Qt.callLater(() => Qt.callLater(forceWidgetRefresh))
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: PluginService
|
||||
function onPluginLoaded(pluginId) {
|
||||
console.log("DankBar: Plugin loaded:", pluginId)
|
||||
// Force componentMap to update by triggering property change
|
||||
if (topBarContent) {
|
||||
topBarContent.updateComponentMap()
|
||||
}
|
||||
}
|
||||
function onPluginUnloaded(pluginId) {
|
||||
console.log("DankBar: Plugin unloaded:", pluginId)
|
||||
// Force componentMap to update by triggering property change
|
||||
if (topBarContent) {
|
||||
topBarContent.updateComponentMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function forceWidgetRefresh() {
|
||||
}
|
||||
|
||||
@@ -366,7 +384,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 +445,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 +537,9 @@ Item {
|
||||
widgetsModel: SettingsData.dankBarLeftWidgetsModel
|
||||
components: topBarContent.allComponents
|
||||
noBackground: SettingsData.dankBarNoBackground
|
||||
parentScreen: barWindow.screen
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
}
|
||||
|
||||
RightSection {
|
||||
@@ -516,6 +552,9 @@ Item {
|
||||
widgetsModel: SettingsData.dankBarRightWidgetsModel
|
||||
components: topBarContent.allComponents
|
||||
noBackground: SettingsData.dankBarNoBackground
|
||||
parentScreen: barWindow.screen
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
}
|
||||
|
||||
CenterSection {
|
||||
@@ -528,6 +567,9 @@ Item {
|
||||
widgetsModel: SettingsData.dankBarCenterWidgetsModel
|
||||
components: topBarContent.allComponents
|
||||
noBackground: SettingsData.dankBarNoBackground
|
||||
parentScreen: barWindow.screen
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,6 +589,9 @@ Item {
|
||||
widgetsModel: SettingsData.dankBarLeftWidgetsModel
|
||||
components: topBarContent.allComponents
|
||||
noBackground: SettingsData.dankBarNoBackground
|
||||
parentScreen: barWindow.screen
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
}
|
||||
|
||||
CenterSection {
|
||||
@@ -560,6 +605,9 @@ Item {
|
||||
widgetsModel: SettingsData.dankBarCenterWidgetsModel
|
||||
components: topBarContent.allComponents
|
||||
noBackground: SettingsData.dankBarNoBackground
|
||||
parentScreen: barWindow.screen
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
}
|
||||
|
||||
RightSection {
|
||||
@@ -1016,6 +1064,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,22 +25,72 @@ 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) {
|
||||
console.log("WidgetHost: Injecting PluginService into plugin widget:", widgetId)
|
||||
item.pluginService = PluginService
|
||||
if (item.loadTimezones) {
|
||||
console.log("WidgetHost: Calling loadTimezones for widget:", widgetId)
|
||||
item.loadTimezones()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getWidgetComponent(widgetId, components) {
|
||||
// Build component map for built-in widgets
|
||||
const componentMap = {
|
||||
"launcherButton": components.launcherButtonComponent,
|
||||
"workspaceSwitcher": components.workspaceSwitcherComponent,
|
||||
@@ -67,7 +121,14 @@ Loader {
|
||||
"systemUpdate": components.systemUpdateComponent
|
||||
}
|
||||
|
||||
return componentMap[widgetId] || null
|
||||
// Check for built-in component first
|
||||
if (componentMap[widgetId]) {
|
||||
return componentMap[widgetId]
|
||||
}
|
||||
|
||||
// Check for plugin component
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
1214
Modules/Greetd/GreeterContent.qml
Normal file
1214
Modules/Greetd/GreeterContent.qml
Normal file
File diff suppressed because it is too large
Load Diff
28
Modules/Greetd/GreeterState.qml
Normal file
28
Modules/Greetd/GreeterState.qml
Normal file
@@ -0,0 +1,28 @@
|
||||
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 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 `/etc/greetd/start-dms.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 = "/etc/greetd/start-dms.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.
|
||||
6
Modules/Greetd/assets/dms-hypr.conf
Normal file
6
Modules/Greetd/assets/dms-hypr.conf
Normal file
@@ -0,0 +1,6 @@
|
||||
env = DMS_RUN_GREETER,1
|
||||
env = QT_QPA_PLATFORM,wayland
|
||||
env = QT_WAYLAND_DISABLE_WINDOWDECORATION,1
|
||||
env = EGL_PLATFORM,gbm
|
||||
|
||||
exec = sh -c "qs -p _DMS_PATH_; hyprctl dispatch exit"
|
||||
21
Modules/Greetd/assets/dms-niri.kdl
Normal file
21
Modules/Greetd/assets/dms-niri.kdl
Normal file
@@ -0,0 +1,21 @@
|
||||
hotkey-overlay {
|
||||
skip-at-startup
|
||||
}
|
||||
|
||||
environment {
|
||||
DMS_RUN_GREETER "1"
|
||||
QT_QPA_PLATFORM "wayland"
|
||||
QT_WAYLAND_DISABLE_WINDOWDECORATION "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
|
||||
}
|
||||
}
|
||||
3
Modules/Greetd/assets/greet-hyprland.sh
Executable file
3
Modules/Greetd/assets/greet-hyprland.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
EGL_PLATFORM=gbm Hyprland -c /etc/greetd/dms-hypr.conf
|
||||
3
Modules/Greetd/assets/greet-niri.sh
Executable file
3
Modules/Greetd/assets/greet-niri.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
EGL_PLATFORM=gbm niri -c /etc/greetd/dms-niri.kdl
|
||||
@@ -44,10 +44,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: {
|
||||
@@ -180,93 +178,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 {
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
131
Modules/Plugins/ListSetting.qml
Normal file
131
Modules/Plugins/ListSetting.qml
Normal file
@@ -0,0 +1,131 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
required property string settingKey
|
||||
required property string label
|
||||
property string description: ""
|
||||
property var items: []
|
||||
property Component delegate: null
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Component.onCompleted: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
items = settings.loadValue(settingKey, [])
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
230
Modules/Plugins/ListSettingWithInput.qml
Normal file
230
Modules/Plugins/ListSettingWithInput.qml
Normal file
@@ -0,0 +1,230 @@
|
||||
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 items: []
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Component.onCompleted: {
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
items = settings.loadValue(settingKey, [])
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
69
Modules/Plugins/PluginComponent.qml
Normal file
69
Modules/Plugins/PluginComponent.qml
Normal file
@@ -0,0 +1,69 @@
|
||||
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 Component horizontalBarPill: null
|
||||
property Component verticalBarPill: null
|
||||
property Component popoutContent: null
|
||||
property real popoutWidth: 400
|
||||
property real popoutHeight: 400
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PluginPopout {
|
||||
id: pluginPopout
|
||||
contentWidth: root.popoutWidth
|
||||
contentHeight: root.popoutHeight
|
||||
pluginContent: root.popoutContent
|
||||
}
|
||||
}
|
||||
116
Modules/Plugins/PluginPopout.qml
Normal file
116
Modules/Plugins/PluginPopout.qml
Normal file
@@ -0,0 +1,116 @@
|
||||
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.spacingL * 2
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 32
|
||||
visible: closeButton.visible
|
||||
|
||||
Item {
|
||||
width: parent.width - 32
|
||||
height: 32
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: closeButton
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: closeArea.containsMouse ? Theme.errorHover : "transparent"
|
||||
visible: true
|
||||
|
||||
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: {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: popoutContent
|
||||
width: parent.width
|
||||
sourceComponent: root.pluginContent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Modules/Plugins/PluginSettings.qml
Normal file
33
Modules/Plugins/PluginSettings.qml
Normal file
@@ -0,0 +1,33 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string pluginId
|
||||
property var pluginService: null
|
||||
default property alias content: settingsColumn.children
|
||||
|
||||
implicitHeight: settingsColumn.implicitHeight
|
||||
height: implicitHeight
|
||||
|
||||
function saveValue(key, value) {
|
||||
if (pluginService && pluginService.savePluginData) {
|
||||
pluginService.savePluginData(pluginId, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
function loadValue(key, defaultValue) {
|
||||
if (pluginService && pluginService.loadPluginData) {
|
||||
return pluginService.loadPluginData(pluginId, key, defaultValue)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
Column {
|
||||
id: settingsColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
}
|
||||
}
|
||||
113
Modules/Plugins/SelectionSetting.qml
Normal file
113
Modules/Plugins/SelectionSetting.qml
Normal file
@@ -0,0 +1,113 @@
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
width: parent.width * 0.4
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
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 !== ""
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
width: parent.width * 0.6 - Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
currentValue: root.valueToLabel[root.value] || root.value
|
||||
options: root.optionLabels
|
||||
onValueChanged: newValue => {
|
||||
root.value = root.labelToValue[newValue] || 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,8 @@ import qs.Widgets
|
||||
Item {
|
||||
id: dankBarTab
|
||||
|
||||
property var baseWidgetDefinitions: [{
|
||||
property var baseWidgetDefinitions: {
|
||||
var coreWidgets = [{
|
||||
"id": "launcherButton",
|
||||
"text": "App Launcher",
|
||||
"description": "Quick access to application launcher",
|
||||
@@ -177,6 +178,22 @@ Item {
|
||||
"icon": "update",
|
||||
"enabled": SystemUpdateService.distributionSupported
|
||||
}]
|
||||
|
||||
// Add plugin widgets dynamically
|
||||
var loadedPlugins = PluginService.getLoadedPlugins()
|
||||
for (var i = 0; i < loadedPlugins.length; i++) {
|
||||
var plugin = loadedPlugins[i]
|
||||
coreWidgets.push({
|
||||
"id": plugin.id,
|
||||
"text": plugin.name,
|
||||
"description": plugin.description || "Plugin widget",
|
||||
"icon": plugin.icon || "extension",
|
||||
"enabled": true
|
||||
})
|
||||
}
|
||||
|
||||
return coreWidgets
|
||||
}
|
||||
property var defaultLeftWidgets: [{
|
||||
"id": "launcherButton",
|
||||
"enabled": true
|
||||
|
||||
@@ -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
|
||||
|
||||
452
Modules/Settings/PluginsTab.qml
Normal file
452
Modules/Settings/PluginsTab.qml
Normal file
@@ -0,0 +1,452 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: pluginsTab
|
||||
|
||||
property string expandedPluginId: ""
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("PluginsTab: Component completed")
|
||||
console.log("PluginsTab: PluginService available:", typeof PluginService !== "undefined")
|
||||
if (typeof PluginService !== "undefined") {
|
||||
console.log("PluginsTab: Available plugins:", Object.keys(PluginService.availablePlugins).length)
|
||||
console.log("PluginsTab: Plugin directory:", PluginService.pluginDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
onIsExpandedChanged: {
|
||||
console.log("Plugin", pluginId, "isExpanded changed to:", isExpanded)
|
||||
}
|
||||
|
||||
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: {
|
||||
console.log("Plugin clicked:", pluginDelegate.pluginId, "hasSettings:", pluginDelegate.hasSettings, "isLoaded:", PluginService.isPluginLoaded(pluginDelegate.pluginId))
|
||||
if (pluginDelegate.hasSettings) {
|
||||
if (pluginsTab.expandedPluginId === pluginDelegate.pluginId) {
|
||||
console.log("Collapsing plugin:", pluginDelegate.pluginId)
|
||||
pluginsTab.expandedPluginId = ""
|
||||
} else {
|
||||
console.log("Expanding plugin:", pluginDelegate.pluginId)
|
||||
pluginsTab.expandedPluginId = 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 - pluginToggle.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
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: pluginToggle
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: PluginService.isPluginLoaded(pluginDelegate.pluginId)
|
||||
onToggled: (isChecked) => {
|
||||
if (isChecked) {
|
||||
if (PluginService.enablePlugin(pluginDelegate.pluginId)) {
|
||||
ToastService.showInfo("Plugin enabled: " + pluginDelegate.pluginName)
|
||||
} else {
|
||||
ToastService.showError("Failed to enable plugin: " + pluginDelegate.pluginName)
|
||||
checked = false
|
||||
}
|
||||
} else {
|
||||
if (PluginService.disablePlugin(pluginDelegate.pluginId)) {
|
||||
ToastService.showInfo("Plugin disabled: " + pluginDelegate.pluginName)
|
||||
if (pluginsTab.expandedPluginId === pluginDelegate.pluginId) {
|
||||
pluginsTab.expandedPluginId = ""
|
||||
}
|
||||
} else {
|
||||
ToastService.showError("Failed to disable plugin: " + pluginDelegate.pluginName)
|
||||
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
|
||||
|
||||
onActiveChanged: {
|
||||
console.log("Settings loader active changed to:", active, "for plugin:", pluginDelegate.pluginId,
|
||||
"isExpanded:", pluginDelegate.isExpanded, "hasSettings:", pluginDelegate.hasSettings,
|
||||
"isLoaded:", PluginService.isPluginLoaded(pluginDelegate.pluginId))
|
||||
}
|
||||
|
||||
source: {
|
||||
if (active && pluginDelegate.pluginSettingsPath) {
|
||||
console.log("Loading plugin settings from:", pluginDelegate.pluginSettingsPath)
|
||||
var path = pluginDelegate.pluginSettingsPath
|
||||
if (!path.startsWith("file://")) {
|
||||
path = "file://" + path
|
||||
}
|
||||
return path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
console.log("Settings loader status changed:", status, "for plugin:", pluginDelegate.pluginId)
|
||||
if (status === Loader.Error) {
|
||||
console.error("Failed to load plugin settings:", pluginDelegate.pluginSettingsPath)
|
||||
} else if (status === Loader.Ready) {
|
||||
console.log("Settings successfully loaded for plugin:", pluginDelegate.pluginId)
|
||||
}
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
console.log("Plugin settings loaded for:", pluginDelegate.pluginId)
|
||||
|
||||
if (typeof PluginService !== "undefined") {
|
||||
console.log("Making PluginService available to plugin settings")
|
||||
console.log("PluginService functions available:",
|
||||
"savePluginData" in PluginService,
|
||||
"loadPluginData" in PluginService)
|
||||
item.pluginService = PluginService
|
||||
console.log("PluginService assignment completed, item.pluginService:", item.pluginService !== null)
|
||||
} else {
|
||||
console.error("PluginService not available in PluginsTab context")
|
||||
}
|
||||
|
||||
if (item.loadTimezones) {
|
||||
console.log("Calling loadTimezones for WorldClock plugin")
|
||||
item.loadTimezones()
|
||||
}
|
||||
if (item.initializeSettings) {
|
||||
item.initializeSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: PluginService
|
||||
function onPluginLoaded() {
|
||||
pluginRepeater.model = PluginService.getAvailablePlugins()
|
||||
}
|
||||
function onPluginUnloaded() {
|
||||
pluginRepeater.model = PluginService.getAvailablePlugins()
|
||||
if (pluginsTab.expandedPluginId !== "" && !PluginService.isPluginLoaded(pluginsTab.expandedPluginId)) {
|
||||
pluginsTab.expandedPluginId = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
10
README.md
10
README.md
@@ -18,7 +18,7 @@ A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/)
|
||||
<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
|
||||
|
||||
@@ -440,6 +440,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).
|
||||
|
||||
318
Services/PluginService.qml
Normal file
318
Services/PluginService.qml
Normal file
@@ -0,0 +1,318 @@
|
||||
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)
|
||||
|
||||
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"
|
||||
console.log("PluginService: Found plugin directory:", dir, "checking manifest at:", manifestPath)
|
||||
loadPluginManifest(manifestPath)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log("PluginService: No directories found in:", currentDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function(exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
console.log("PluginService: Directory scan failed for:", pluginDirectories[currentScanIndex], "exit code:", exitCode)
|
||||
}
|
||||
currentScanIndex++
|
||||
if (currentScanIndex < pluginDirectories.length) {
|
||||
scanNextDirectory()
|
||||
} else {
|
||||
currentScanIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scanPlugins() {
|
||||
currentScanIndex = 0
|
||||
scanNextDirectory()
|
||||
}
|
||||
|
||||
function scanNextDirectory() {
|
||||
var dir = pluginDirectories[currentScanIndex]
|
||||
console.log("PluginService: Scanning directory:", dir)
|
||||
lsProcess.command = ["find", "-L", dir, "-maxdepth", "1", "-type", "d", "-not", "-path", dir, "-exec", "basename", "{}", ";"]
|
||||
lsProcess.running = true
|
||||
}
|
||||
|
||||
property var manifestReaders: ({})
|
||||
|
||||
function loadPluginManifest(manifestPath) {
|
||||
console.log("PluginService: Loading manifest:", manifestPath)
|
||||
|
||||
// Create a unique key for this manifest reader
|
||||
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 {
|
||||
console.log("PluginService: DEBUGGING parsing manifest, text length:", process.stdout.text.length)
|
||||
var manifest = JSON.parse(process.stdout.text.trim())
|
||||
console.log("PluginService: Successfully parsed manifest for plugin:", manifest.id)
|
||||
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) {
|
||||
console.log("PluginService: registerPlugin called with", manifest.id)
|
||||
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
|
||||
console.log("PluginService: Registered plugin:", manifest.id, "-", manifest.name)
|
||||
console.log("PluginService: Component path:", pluginInfo.componentPath)
|
||||
}
|
||||
|
||||
function loadPlugin(pluginId) {
|
||||
console.log("PluginService: loadPlugin called for", pluginId)
|
||||
var plugin = availablePlugins[pluginId]
|
||||
if (!plugin) {
|
||||
console.error("PluginService: Plugin not found:", pluginId)
|
||||
pluginLoadFailed(pluginId, "Plugin not found")
|
||||
return false
|
||||
}
|
||||
|
||||
if (plugin.loaded) {
|
||||
console.log("PluginService: Plugin already loaded:", pluginId)
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
// Create the widget component
|
||||
var componentUrl = "file://" + plugin.componentPath
|
||||
console.log("PluginService: Loading component from:", componentUrl)
|
||||
|
||||
var component = Qt.createComponent(componentUrl)
|
||||
if (component.status === Component.Error) {
|
||||
console.error("PluginService: Failed to create component for plugin:", pluginId, "Error:", component.errorString())
|
||||
pluginLoadFailed(pluginId, component.errorString())
|
||||
return false
|
||||
}
|
||||
|
||||
pluginWidgetComponents[pluginId] = component
|
||||
plugin.loaded = true
|
||||
loadedPlugins[pluginId] = plugin
|
||||
|
||||
console.log("PluginService: Successfully loaded plugin:", pluginId)
|
||||
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 {
|
||||
// Remove from component map
|
||||
delete pluginWidgetComponents[pluginId]
|
||||
|
||||
// Mark as unloaded
|
||||
plugin.loaded = false
|
||||
delete loadedPlugins[pluginId]
|
||||
|
||||
console.log("PluginService: Successfully unloaded plugin:", 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) {
|
||||
console.log("PluginService: Enabling plugin:", pluginId)
|
||||
SettingsData.setPluginSetting(pluginId, "enabled", true)
|
||||
return loadPlugin(pluginId)
|
||||
}
|
||||
|
||||
function disablePlugin(pluginId) {
|
||||
console.log("PluginService: Disabling plugin:", 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) {
|
||||
console.log("PluginService: Saving plugin data:", pluginId, key, JSON.stringify(value))
|
||||
SettingsData.setPluginSetting(pluginId, key, value)
|
||||
console.log("PluginService: Data saved successfully")
|
||||
return true
|
||||
}
|
||||
|
||||
function loadPluginData(pluginId, key, defaultValue) {
|
||||
console.log("PluginService: Loading plugin data:", pluginId, key)
|
||||
var value = SettingsData.getPluginSetting(pluginId, key, defaultValue)
|
||||
console.log("PluginService: Loaded key:", key, "value:", JSON.stringify(value))
|
||||
return value
|
||||
}
|
||||
|
||||
function createPluginDirectory() {
|
||||
console.log("PluginService: Creating plugin directory:", pluginDirectory)
|
||||
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.log("PluginService: Successfully created plugin directory")
|
||||
} else {
|
||||
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"]
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,9 @@ Rectangle {
|
||||
property bool enableFuzzySearch: false
|
||||
property int popupWidthOffset: 0
|
||||
property int maxPopupHeight: 400
|
||||
property bool openUpwards: false
|
||||
property int popupWidth: 0
|
||||
property bool alignPopupRight: false
|
||||
|
||||
signal valueChanged(string value)
|
||||
|
||||
@@ -102,10 +105,33 @@ Rectangle {
|
||||
return
|
||||
}
|
||||
|
||||
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.openUpwards || root.alignPopupRight) {
|
||||
popup.open()
|
||||
Qt.callLater(() => {
|
||||
if (root.openUpwards) {
|
||||
const pos = dropdown.mapToItem(Overlay.overlay, 0, 0)
|
||||
if (root.alignPopupRight) {
|
||||
popup.x = pos.x + dropdown.width - popup.width
|
||||
} else {
|
||||
popup.x = pos.x - (root.popupWidthOffset / 2)
|
||||
}
|
||||
popup.y = pos.y - popup.height - 4
|
||||
} else {
|
||||
const pos = dropdown.mapToItem(Overlay.overlay, 0, dropdown.height + 4)
|
||||
if (root.alignPopupRight) {
|
||||
popup.x = pos.x + dropdown.width - popup.width
|
||||
} else {
|
||||
popup.x = pos.x - (root.popupWidthOffset / 2)
|
||||
}
|
||||
popup.y = pos.y
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const pos = dropdown.mapToItem(Overlay.overlay, 0, dropdown.height + 4)
|
||||
popup.x = pos.x - (root.popupWidthOffset / 2)
|
||||
popup.y = pos.y
|
||||
popup.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +239,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
parent: Overlay.overlay
|
||||
width: dropdown.width + root.popupWidthOffset
|
||||
width: 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
|
||||
@@ -338,8 +364,9 @@ Rectangle {
|
||||
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
|
||||
width: root.popupWidth > 0 ? undefined : (parent.parent.width - parent.x - Theme.spacingS)
|
||||
elide: root.popupWidth > 0 ? Text.ElideNone : Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
675
docs/PLUGINS.md
Normal file
675
docs/PLUGINS.md
Normal file
@@ -0,0 +1,675 @@
|
||||
# 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",
|
||||
"dependencies": {
|
||||
"libraryName": {
|
||||
"url": "https://cdn.example.com/library.js",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"settings_schema": {
|
||||
"settingKey": {
|
||||
"type": "string|number|boolean|array|object",
|
||||
"default": "defaultValue"
|
||||
}
|
||||
},
|
||||
"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
|
||||
- `version`: Semantic version string
|
||||
- `author`: Plugin creator name
|
||||
- `icon`: Material Design icon name
|
||||
- `settings`: Path to settings component
|
||||
- `dependencies`: External JS libraries
|
||||
- `settings_schema`: Configuration schema
|
||||
- `permissions`: Required capabilities
|
||||
|
||||
### 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 {
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
padding: 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
|
||||
|
||||
### 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!
|
||||
|
||||
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)
|
||||
- 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)
|
||||
- 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)
|
||||
- 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)
|
||||
- `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)
|
||||
- `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"
|
||||
|
||||
// Section header (optional)
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: "General Settings"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
// Text input
|
||||
StringSetting {
|
||||
settingKey: "apiKey"
|
||||
label: "API Key"
|
||||
description: "Your service API key"
|
||||
placeholder: "sk-..."
|
||||
defaultValue: ""
|
||||
}
|
||||
|
||||
// Toggle switches
|
||||
ToggleSetting {
|
||||
settingKey: "enabled"
|
||||
label: "Enable Feature"
|
||||
description: "Turn this feature on or off"
|
||||
defaultValue: true
|
||||
}
|
||||
|
||||
// Dropdown selection
|
||||
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"
|
||||
}
|
||||
|
||||
// Structured list with multi-field input
|
||||
ListSettingWithInput {
|
||||
settingKey: "locations"
|
||||
label: "Locations"
|
||||
description: "Track multiple locations"
|
||||
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
|
||||
- `settings_write`: Write plugin configuration
|
||||
- `process`: Execute system commands
|
||||
- `network`: Network access
|
||||
|
||||
Future versions may enforce permission restrictions.
|
||||
|
||||
## 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.
|
||||
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