1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-03 19:12:11 -04:00

Compare commits

...

10 Commits

Author SHA1 Message Date
purian23
3c2d60d8e1 fix: QT notifs warning 2026-02-10 21:38:25 -05:00
purian23
9c4f4cbd0d notifications: Add Left/Right Keyboard Nav to Current/History tabs 2026-02-10 20:51:13 -05:00
bbedward
a337585b00 core/server/dbus: suppress unsubscribe warnings 2026-02-10 17:52:39 -05:00
bbedward
1cdec5d687 launcher v2: add visibility guards 2026-02-10 17:40:41 -05:00
bbedward
081b15e24c dock: fix intelligent auto hide on hyprland
fixes #1535
2026-02-10 17:29:39 -05:00
purian23
b04cb7b3cc guide: Include Fedora paths in the Contributing guide 2026-02-10 16:07:33 -05:00
ArijanJ
e2c3ff00fb feat(ipc): add player-specific mpris volume control (#1645)
* feat: add mpris volume control through ipc

* feat: add mpris volume action and default binds
2026-02-10 15:44:56 -05:00
bbedward
c783ff3dcf core: add DL helper, apply to TrackArt OSD, DankLocationSearch
- unrelated change to add gsettingsOrDconf helpers
2026-02-10 15:42:40 -05:00
bbedward
2c360dc3e8 mautgen: post-hook reload GTK4 and qt6ct
fixes #1643
2026-02-10 15:06:44 -05:00
bbedward
5342647bfb launcher v2: performance optimizations
- Use ListView in all tab
- use filesystem cache to speed up first launch
- apply highlights to visible models
2026-02-10 14:56:29 -05:00
34 changed files with 963 additions and 487 deletions

View File

@@ -37,10 +37,43 @@ This is a monorepo, the easiest thing to do is to open an editor in either `quic
1. Install the [QML Extension](https://doc.qt.io/vscodeext/)
2. Configure `ctrl+shift+p` -> user preferences (json) with qmlls path
**Note:** Paths may vary by distribution. Below are examples for Arch Linux and Fedora.
**Arch Linux:**
```json
{
"[qml]": {
"editor.defaultFormatter": "qt-project.qmlls",
"editor.formatOnSave": true
},
"qt-qml.doNotAskForQmllsDownload": true,
"qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls"
"qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls",
"qt-core.additionalQtPaths": [
{
"name": "Qt-6.x-linux-g++",
"path": "/usr/bin/qmake"
}
]
}
```
**Fedora:**
```json
{
"[qml]": {
"editor.defaultFormatter": "qt-project.qmlls",
"editor.formatOnSave": true
},
"qt-qml.doNotAskForQmllsDownload": true,
"qt-qml.qmlls.customExePath": "/usr/bin/qmlls",
"qt-core.additionalQtPaths": [
{
"name": "Qt-6.x-Fedora-linux-g++",
"path": "/usr/bin/qmake6"
}
]
}
```

View File

@@ -524,5 +524,6 @@ func getCommonCommands() []*cobra.Command {
chromaCmd,
doctorCmd,
configCmd,
dlCmd,
}
}

View File

@@ -0,0 +1,99 @@
package main
import (
"context"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"time"
"github.com/spf13/cobra"
)
var dlOutput string
var dlUserAgent string
var dlTimeout int
var dlIPv4Only bool
var dlCmd = &cobra.Command{
Use: "dl <url>",
Short: "Download a URL to stdout or file",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if err := runDownload(args[0]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
},
}
func init() {
dlCmd.Flags().StringVarP(&dlOutput, "output", "o", "", "Output file path (default: stdout)")
dlCmd.Flags().StringVar(&dlUserAgent, "user-agent", "", "Custom User-Agent header")
dlCmd.Flags().IntVar(&dlTimeout, "timeout", 10, "Request timeout in seconds")
dlCmd.Flags().BoolVarP(&dlIPv4Only, "ipv4", "4", false, "Force IPv4 only")
}
func runDownload(url string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(dlTimeout)*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("invalid request: %w", err)
}
switch {
case dlUserAgent != "":
req.Header.Set("User-Agent", dlUserAgent)
default:
req.Header.Set("User-Agent", "DankMaterialShell/1.0 (Linux)")
}
dialer := &net.Dialer{Timeout: 5 * time.Second}
transport := &http.Transport{DialContext: dialer.DialContext}
if dlIPv4Only {
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, "tcp4", addr)
}
}
client := &http.Client{Transport: transport}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("download failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("HTTP %d", resp.StatusCode)
}
if dlOutput == "" {
_, err = io.Copy(os.Stdout, resp.Body)
return err
}
if dir := filepath.Dir(dlOutput); dir != "." {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("mkdir failed: %w", err)
}
}
f, err := os.Create(dlOutput)
if err != nil {
return fmt.Errorf("create failed: %w", err)
}
defer f.Close()
if _, err := io.Copy(f, resp.Body); err != nil {
os.Remove(dlOutput)
return fmt.Errorf("write failed: %w", err)
}
fmt.Println(dlOutput)
return nil
}

View File

@@ -27,6 +27,8 @@ bindl = , XF86AudioPause, exec, dms ipc call mpris playPause
bindl = , XF86AudioPlay, exec, dms ipc call mpris playPause
bindl = , XF86AudioPrev, exec, dms ipc call mpris previous
bindl = , XF86AudioNext, exec, dms ipc call mpris next
bindel = CTRL, XF86AudioRaiseVolume, exec, dms ipc call mpris increment 3
bindel = CTRL, XF86AudioLowerVolume, exec, dms ipc call mpris decrement 3
# === Brightness Controls ===
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""

View File

@@ -60,6 +60,12 @@ binds {
XF86AudioNext allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "next";
}
Ctrl+XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "increment" "3";
}
Ctrl+XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "decrement" "3";
}
// === Brightness Controls ===
XF86MonBrightnessUp allow-when-locked=true {

View File

@@ -11,6 +11,7 @@ import (
"strings"
"sync"
"syscall"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -260,6 +261,11 @@ func buildOnce(opts *Options) error {
syncAccentColor(primaryDark)
}
refreshGTK(opts.Mode)
refreshGTK4()
}
if !opts.ShouldSkipTemplate("qt6ct") && appExists(opts.AppChecker, []string{"qt6ct"}, nil) {
refreshQt6ct()
}
signalTerminals()
@@ -646,8 +652,44 @@ func isDMSGTKActive(configDir string) bool {
}
func refreshGTK(mode ColorMode) {
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "").Run()
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", mode.GTKTheme()).Run()
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", ""); err != nil {
log.Warnf("Failed to reset gtk-theme: %v", err)
}
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", mode.GTKTheme()); err != nil {
log.Warnf("Failed to set gtk-theme: %v", err)
}
}
func refreshGTK4() {
output, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme")
if err != nil {
return
}
current := strings.Trim(output, "'")
var toggle string
if current == "prefer-dark" {
toggle = "default"
} else {
toggle = "prefer-dark"
}
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", toggle); err != nil {
log.Warnf("Failed to toggle color-scheme for GTK4 refresh: %v", err)
return
}
time.Sleep(50 * time.Millisecond)
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", current); err != nil {
log.Warnf("Failed to restore color-scheme for GTK4 refresh: %v", err)
}
}
func refreshQt6ct() {
confPath := filepath.Join(utils.XDGConfigHome(), "qt6ct", "qt6ct.conf")
now := time.Now()
if err := os.Chtimes(confPath, now, now); err != nil {
log.Warnf("Failed to touch qt6ct.conf: %v", err)
}
}
func signalTerminals() {
@@ -683,8 +725,8 @@ func syncColorScheme(mode ColorMode) {
scheme = "default"
}
if err := exec.Command("gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme).Run(); err != nil {
exec.Command("dconf", "write", "/org/gnome/desktop/interface/color-scheme", "'"+scheme+"'").Run()
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", scheme); err != nil {
log.Warnf("Failed to sync color-scheme: %v", err)
}
}
@@ -734,7 +776,9 @@ func closestAdwaitaAccent(primaryHex string) string {
func syncAccentColor(primaryHex string) {
accent := closestAdwaitaAccent(primaryHex)
log.Infof("Setting GNOME accent color: %s", accent)
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "accent-color", accent).Run()
if err := utils.GsettingsSet("org.gnome.desktop.interface", "accent-color", accent); err != nil {
log.Warnf("Failed to set accent-color: %v", err)
}
}
type TemplateCheck struct {

View File

@@ -3,11 +3,11 @@ package screenshot
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
type ThemeColors struct {
@@ -83,12 +83,11 @@ func getColorsFilePath() string {
}
func isLightMode() bool {
out, err := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "color-scheme").Output()
scheme, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme")
if err != nil {
return false
}
scheme := strings.TrimSpace(string(out))
switch scheme {
case "'prefer-light'", "'default'":
return true

View File

@@ -276,9 +276,7 @@ func (m *Manager) UnsubscribeClient(clientID string) {
})
for _, subID := range toDelete {
if err := m.Unsubscribe(subID); err != nil {
log.Warnf("dbus: failed to unsubscribe %s: %v", subID, err)
}
_ = m.Unsubscribe(subID)
}
}

View File

@@ -1,11 +1,9 @@
package freedesktop
import (
"context"
"fmt"
"os/exec"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/godbus/dbus/v5"
)
@@ -107,22 +105,8 @@ func (m *Manager) GetUserIconFile(username string) (string, error) {
}
func (m *Manager) SetIconTheme(iconTheme string) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
check := exec.CommandContext(ctx, "gsettings", "writable", "org.gnome.desktop.interface", "icon-theme")
if err := check.Run(); err == nil {
cmd := exec.CommandContext(ctx, "gsettings", "set", "org.gnome.desktop.interface", "icon-theme", iconTheme)
if err := cmd.Run(); err != nil {
return fmt.Errorf("gsettings set failed: %w", err)
}
return nil
if err := utils.GsettingsSet("org.gnome.desktop.interface", "icon-theme", iconTheme); err != nil {
return fmt.Errorf("failed to set icon theme: %w", err)
}
checkDconf := exec.CommandContext(ctx, "dconf", "write", "/org/gnome/desktop/interface/icon-theme", fmt.Sprintf("'%s'", iconTheme))
if err := checkDconf.Run(); err != nil {
return fmt.Errorf("both gsettings and dconf unavailable or failed: %w", err)
}
return nil
}

View File

@@ -0,0 +1,31 @@
package utils
import (
"fmt"
"os/exec"
"strings"
)
func dconfPath(schema, key string) string {
return "/" + strings.ReplaceAll(schema, ".", "/") + "/" + key
}
// GsettingsGet reads a gsettings value, falling back to dconf read.
func GsettingsGet(schema, key string) (string, error) {
if out, err := exec.Command("gsettings", "get", schema, key).Output(); err == nil {
return strings.TrimSpace(string(out)), nil
}
out, err := exec.Command("dconf", "read", dconfPath(schema, key)).Output()
if err != nil {
return "", fmt.Errorf("gsettings/dconf get failed for %s %s: %w", schema, key, err)
}
return strings.TrimSpace(string(out)), nil
}
// GsettingsSet writes a gsettings value, falling back to dconf write.
func GsettingsSet(schema, key, value string) error {
if err := exec.Command("gsettings", "set", schema, key, value).Run(); err == nil {
return nil
}
return exec.Command("dconf", "write", dconfPath(schema, key), "'"+value+"'").Run()
}

View File

@@ -10,6 +10,7 @@ Singleton {
id: root
property var appUsageRanking: {}
property bool _saving: false
Component.onCompleted: {
loadSettings();
@@ -59,7 +60,9 @@ Singleton {
}
appUsageRanking = currentRanking;
_saving = true;
saveSettings();
_saving = false;
}
function getRankedApps() {
@@ -97,7 +100,9 @@ Singleton {
if (hasChanges) {
appUsageRanking = currentRanking;
_saving = true;
saveSettings();
_saving = false;
}
}
@@ -109,6 +114,8 @@ Singleton {
blockWrites: true
watchChanges: true
onLoaded: {
if (root._saving)
return;
parseSettings(settingsFile.text());
}
onLoadFailed: error => {}

View File

@@ -178,6 +178,33 @@ Singleton {
}
}
function loadLauncherCache() {
try {
var content = launcherCacheFile.text();
if (content && content.trim())
return JSON.parse(content);
} catch (e) {
console.warn("CacheData: Failed to parse launcher cache:", e.message);
}
return null;
}
function saveLauncherCache(sections) {
if (_loading)
return;
launcherCacheFile.setText(JSON.stringify(sections));
}
FileView {
id: launcherCacheFile
path: isGreeterMode ? "" : _stateDir + "/DankMaterialShell/launcher_cache.json"
blockLoading: true
blockWrites: true
atomicWrites: true
watchChanges: false
}
FileView {
id: cacheFile

View File

@@ -57,6 +57,8 @@ const DMS_ACTIONS = [
{ id: "spawn dms ipc call audio decrement 1", label: "Volume Down (1%)" },
{ id: "spawn dms ipc call audio decrement 5", label: "Volume Down (5%)" },
{ id: "spawn dms ipc call audio decrement 10", label: "Volume Down (10%)" },
{ id: "spawn dms ipc call mpris increment 5", label: "Player Volume Up (5%)" },
{ id: "spawn dms ipc call mpris decrement 5", label: "Player Volume Down (5%)" },
{ id: "spawn dms ipc call audio mute", label: "Volume Mute Toggle" },
{ id: "spawn dms ipc call audio micmute", label: "Microphone Mute Toggle" },
{ id: "spawn dms ipc call audio cycleoutput", label: "Audio Output: Cycle" },
@@ -719,6 +721,14 @@ const DMS_ACTION_ARGS = {
base: "spawn dms ipc call audio decrement",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "5" }]
},
"player increment": {
base: "spawn dms ipc call mpris increment",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "5" }]
},
"player decrement": {
base: "spawn dms ipc call mpris decrement",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "5" }]
},
"brightness increment": {
base: "spawn dms ipc call brightness increment",
args: [

View File

@@ -339,6 +339,36 @@ Item {
}
}
function increment(step: string): string {
if (MprisController.activePlayer && MprisController.activePlayer.volumeSupported) {
const currentVolume = Math.round(MprisController.activePlayer.volume * 100);
const stepValue = parseInt(step || "5");
const newVolume = Math.max(0, Math.min(100, currentVolume + stepValue));
MprisController.activePlayer.volume = newVolume / 100;
return `Player volume increased to ${newVolume}%`;
}
}
function decrement(step: string): string {
if (MprisController.activePlayer && MprisController.activePlayer.volumeSupported) {
const currentVolume = Math.round(MprisController.activePlayer.volume * 100);
const stepValue = parseInt(step || "5");
const newVolume = Math.max(0, Math.min(100, currentVolume - stepValue));
MprisController.activePlayer.volume = newVolume / 100;
return `Player volume decreased to ${newVolume}%`;
}
}
function setvolume(percentage: string): string {
if (MprisController.activePlayer && MprisController.activePlayer.volumeSupported) {
const clampedVolume = Math.max(0, Math.min(100, percentage));
MprisController.activePlayer.volume = clampedVolume / 100;
return `Player volume set to ${clampedVolume}%`;
}
}
target: "mpris"
}

View File

@@ -26,8 +26,10 @@ Item {
property string activePluginId: ""
property var collapsedSections: ({})
property bool keyboardNavigationActive: false
property bool active: false
property var _modeSectionsCache: ({})
property bool _queryDrivenSearch: false
property bool _diskCacheConsumed: false
property var sectionViewModes: ({})
property var pluginViewPreferences: ({})
property int gridColumns: SettingsData.appLauncherGridColumns
@@ -51,13 +53,19 @@ Item {
Connections {
target: AppSearchService
function onCacheVersionChanged() {
if (!active)
return;
_clearModeCache();
if (!searchQuery && searchMode === "all")
performSearch();
}
}
Connections {
target: PluginService
function onRequestLauncherUpdate(pluginId) {
if (!active)
return;
if (activePluginId === pluginId) {
if (activePluginCategories.length <= 1)
loadPluginCategories(pluginId);
@@ -423,6 +431,30 @@ Item {
var restoreSelection = preserveSelectionAfterUpdate(shouldResetSelection);
var cachedSections = AppSearchService.getCachedDefaultSections();
if (!cachedSections && !_diskCacheConsumed && !searchQuery && searchMode === "all" && !pluginFilter) {
_diskCacheConsumed = true;
var diskSections = _loadDiskCache();
if (diskSections) {
activePluginId = "";
activePluginName = "";
activePluginCategories = [];
activePluginCategory = "";
clearActivePluginViewPreference();
for (var i = 0; i < diskSections.length; i++) {
if (collapsedSections[diskSections[i].id] !== undefined)
diskSections[i].collapsed = collapsedSections[diskSections[i].id];
}
_applyHighlights(diskSections, "");
flatModel = Scorer.flattenSections(diskSections);
sections = diskSections;
selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem();
isSearching = false;
searchCompleted();
return;
}
}
if (cachedSections && !searchQuery && searchMode === "all" && !pluginFilter) {
activePluginId = "";
activePluginName = "";
@@ -431,6 +463,7 @@ Item {
clearActivePluginViewPreference();
var modeCache = _getCachedModeData("all");
if (modeCache) {
_applyHighlights(modeCache.sections, "");
sections = modeCache.sections;
flatModel = modeCache.flatModel;
} else {
@@ -442,6 +475,7 @@ Item {
copy.collapsed = collapsedSections[s.id];
return copy;
});
_applyHighlights(newSections, "");
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
_setCachedModeData("all", sections, flatModel);
@@ -489,6 +523,7 @@ Item {
}
}
_applyHighlights(newSections, triggerMatch.query);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
selectedFlatIndex = restoreSelection(flatModel);
@@ -522,6 +557,7 @@ Item {
if (cachedSections && !searchQuery) {
var modeCache = _getCachedModeData("apps");
if (modeCache) {
_applyHighlights(modeCache.sections, "");
sections = modeCache.sections;
flatModel = modeCache.flatModel;
} else {
@@ -536,6 +572,7 @@ Item {
copy.collapsed = collapsedSections[s.id];
return copy;
});
_applyHighlights(newSections, "");
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
_setCachedModeData("apps", sections, flatModel);
@@ -564,6 +601,7 @@ Item {
}
}
_applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
selectedFlatIndex = restoreSelection(flatModel);
@@ -623,6 +661,7 @@ Item {
}
}
_applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
selectedFlatIndex = restoreSelection(flatModel);
@@ -699,11 +738,13 @@ Item {
}
}
_applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
if (!AppSearchService.isCacheValid() && !searchQuery && searchMode === "all" && !pluginFilter) {
AppSearchService.setCachedDefaultSections(sections, flatModel);
_saveDiskCache(sections);
}
selectedFlatIndex = restoreSelection(flatModel);
@@ -762,6 +803,7 @@ Item {
newSections[i].collapsed = collapsedSections[sid];
}
_applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
selectedFlatIndex = restoreSelection(flatModel);
@@ -816,7 +858,8 @@ Item {
icon: "folder",
priority: 4,
items: fileItems,
collapsed: collapsedSections["files"] || false
collapsed: collapsedSections["files"] || false,
flatStartIndex: 0
};
var newSections;
@@ -835,6 +878,7 @@ Item {
newSections.sort(function (a, b) {
return a.priority - b.priority;
});
_applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
if (selectedFlatIndex >= flatModel.length) {
@@ -1184,6 +1228,88 @@ Item {
_modeSectionsCache = {};
}
function _saveDiskCache(sectionsData) {
var serializable = [];
for (var i = 0; i < sectionsData.length; i++) {
var s = sectionsData[i];
var items = [];
var srcItems = s.items || [];
for (var j = 0; j < srcItems.length; j++) {
var it = srcItems[j];
items.push({
id: it.id,
type: it.type,
name: it.name || "",
subtitle: it.subtitle || "",
icon: it.icon || "",
iconType: it.iconType || "image",
iconFull: it.iconFull || "",
section: it.section || "",
isCore: it.isCore || false,
isBuiltInLauncher: it.isBuiltInLauncher || false,
pluginId: it.pluginId || ""
});
}
serializable.push({
id: s.id,
title: s.title || "",
icon: s.icon || "",
priority: s.priority || 0,
items: items
});
}
CacheData.saveLauncherCache(serializable);
}
function _loadDiskCache() {
var cached = CacheData.loadLauncherCache();
if (!cached || !Array.isArray(cached) || cached.length === 0)
return null;
var sectionsData = [];
for (var i = 0; i < cached.length; i++) {
var s = cached[i];
var items = [];
var srcItems = s.items || [];
for (var j = 0; j < srcItems.length; j++) {
var it = srcItems[j];
items.push({
id: it.id || "",
type: it.type || "app",
name: it.name || "",
subtitle: it.subtitle || "",
icon: it.icon || "",
iconType: it.iconType || "image",
iconFull: it.iconFull || "",
section: it.section || "",
isCore: it.isCore || false,
isBuiltInLauncher: it.isBuiltInLauncher || false,
pluginId: it.pluginId || "",
data: {
id: it.id
},
actions: [],
primaryAction: null,
_diskCached: true,
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
});
}
sectionsData.push({
id: s.id || "",
title: s.title || "",
icon: s.icon || "",
priority: s.priority || 0,
items: items,
collapsed: false,
flatStartIndex: 0
});
}
return sectionsData;
}
function updateSelectedItem() {
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
var entry = flatModel[selectedFlatIndex];
@@ -1193,6 +1319,48 @@ Item {
}
}
function _applyHighlights(sectionsData, query) {
if (!query || query.length === 0) {
for (var i = 0; i < sectionsData.length; i++) {
var items = sectionsData[i].items;
for (var j = 0; j < items.length; j++) {
var item = items[j];
item._hName = item.name || "";
item._hSub = item.subtitle || "";
item._hRich = false;
}
}
return;
}
var highlightColor = Theme.primary;
var nameColor = Theme.surfaceText;
var subColor = Theme.surfaceVariantText;
var lowerQuery = query.toLowerCase();
for (var i = 0; i < sectionsData.length; i++) {
var items = sectionsData[i].items;
for (var j = 0; j < items.length; j++) {
var item = items[j];
item._hName = _highlightField(item.name || "", lowerQuery, query.length, nameColor, highlightColor);
item._hSub = _highlightField(item.subtitle || "", lowerQuery, query.length, subColor, highlightColor);
item._hRich = true;
}
}
}
function _highlightField(text, lowerQuery, queryLen, baseColor, highlightColor) {
if (!text)
return "";
var idx = text.toLowerCase().indexOf(lowerQuery);
if (idx === -1)
return text;
var before = text.substring(0, idx);
var match = text.substring(idx, idx + queryLen);
var after = text.substring(idx + queryLen);
return '<span style="color:' + baseColor + '">' + before + '</span><span style="color:' + highlightColor + '; font-weight:600">' + match + '</span><span style="color:' + baseColor + '">' + after + '</span>';
}
function getCurrentSectionViewMode() {
if (selectedFlatIndex < 0 || selectedFlatIndex >= flatModel.length)
return "list";
@@ -1334,6 +1502,10 @@ Item {
}
function executeSelected() {
if (searchDebounce.running) {
searchDebounce.stop();
performSearch();
}
if (!selectedItem)
return;
executeItem(selectedItem);

View File

@@ -35,21 +35,6 @@ Rectangle {
readonly property int computedIconSize: Math.min(48, Math.max(32, width * 0.45))
function highlightText(text, query, baseColor) {
if (!text || !query || query.length === 0)
return text;
var lowerText = text.toLowerCase();
var lowerQuery = query.toLowerCase();
var idx = lowerText.indexOf(lowerQuery);
if (idx === -1)
return text;
var before = text.substring(0, idx);
var match = text.substring(idx, idx + query.length);
var after = text.substring(idx + query.length);
var highlightColor = Theme.primary;
return '<span style="color:' + baseColor + '">' + before + '</span>' + '<span style="color:' + highlightColor + '; font-weight:600">' + match + '</span>' + '<span style="color:' + baseColor + '">' + after + '</span>';
}
radius: Theme.cornerRadius
color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : "transparent"
@@ -78,15 +63,8 @@ Rectangle {
Text {
width: parent.width
text: {
var query = root.controller?.searchQuery ?? "";
var name = root.item?.name ?? "";
var baseColor = root.isSelected ? Theme.primary : Theme.surfaceText;
if (!query)
return name;
return root.highlightText(name, query, baseColor);
}
textFormat: root.controller?.searchQuery ? Text.RichText : Text.PlainText
text: root.item?._hName ?? root.item?.name ?? ""
textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
font.family: Theme.fontFamily

View File

@@ -1,6 +1,6 @@
.pragma library
.import "ControllerUtils.js" as Utils
.import "ControllerUtils.js" as Utils
function transformApp(app, override, defaultActions, primaryActionLabel) {
var appId = app.id || app.execString || app.exec || "";
@@ -31,7 +31,11 @@ function transformApp(app, override, defaultActions, primaryActionLabel) {
name: primaryActionLabel,
icon: "open_in_new",
action: "launch"
}
},
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
};
}
@@ -66,7 +70,11 @@ function transformCoreApp(app, openLabel) {
name: openLabel,
icon: "open_in_new",
action: "launch"
}
},
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
};
}
@@ -100,7 +108,11 @@ function transformBuiltInLauncherItem(item, pluginId, openLabel) {
name: openLabel,
icon: "open_in_new",
action: "execute"
}
},
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
};
}
@@ -133,7 +145,11 @@ function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel) {
name: openLabel,
icon: "open_in_new",
action: "open"
}
},
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
};
}
@@ -166,7 +182,11 @@ function transformPluginItem(item, pluginId, selectLabel) {
name: selectLabel,
icon: "check",
action: "execute"
}
},
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
};
}
@@ -188,7 +208,11 @@ function createCalculatorItem(calc, query, copyLabel) {
name: copyLabel,
icon: "content_copy",
action: "copy"
}
},
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
};
}
@@ -218,6 +242,10 @@ function createPluginBrowseItem(pluginId, plugin, trigger, isBuiltIn, isAllowed,
name: browseLabel,
icon: "arrow_forward",
action: "browse_plugin"
}
},
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
};
}

View File

@@ -86,6 +86,7 @@ FocusScope {
Controller {
id: controller
active: root.parentModal?.spotlightOpen ?? true
viewModeContext: root.viewModeContext
onItemExecuted: {

View File

@@ -34,7 +34,7 @@ Popup {
return false;
}
readonly property bool isCoreApp: item?.type === "app" && item?.isCore
readonly property bool isCoreApp: item?.type === "app" && !!item?.isCore
readonly property var coreAppData: isCoreApp ? item?.data ?? null : null
readonly property var desktopEntry: !isCoreApp ? (item?.data ?? null) : null
readonly property string appId: {

View File

@@ -25,6 +25,9 @@ function findPrevNonHeaderIndex(flatModel, startIndex) {
}
function getSectionBounds(flatModel, sectionId) {
if (flatModel._sectionBounds && flatModel._sectionBounds[sectionId])
return flatModel._sectionBounds[sectionId];
var start = -1, end = -1;
for (var i = 0; i < flatModel.length; i++) {
if (flatModel[i].isHeader && flatModel[i].section?.id === sectionId) {

View File

@@ -33,21 +33,6 @@ Rectangle {
}
}
function highlightText(text, query, baseColor) {
if (!text || !query || query.length === 0)
return text;
var lowerText = text.toLowerCase();
var lowerQuery = query.toLowerCase();
var idx = lowerText.indexOf(lowerQuery);
if (idx === -1)
return text;
var before = text.substring(0, idx);
var match = text.substring(idx, idx + query.length);
var after = text.substring(idx + query.length);
var highlightColor = Theme.primary;
return '<span style="color:' + baseColor + '">' + before + '</span>' + '<span style="color:' + highlightColor + '; font-weight:600">' + match + '</span>' + '<span style="color:' + baseColor + '">' + after + '</span>';
}
width: parent?.width ?? 200
height: 52
color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : "transparent"
@@ -110,14 +95,8 @@ Rectangle {
Text {
width: parent.width
text: {
var query = root.controller?.searchQuery ?? "";
var name = root.item?.name ?? "";
if (!query)
return name;
return root.highlightText(name, query, Theme.surfaceText);
}
textFormat: root.controller?.searchQuery ? Text.RichText : Text.PlainText
text: root.item?._hName ?? root.item?.name ?? ""
textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
font.family: Theme.fontFamily
@@ -128,16 +107,8 @@ Rectangle {
Text {
width: parent.width
text: {
var query = root.controller?.searchQuery ?? "";
var subtitle = root.item?.subtitle ?? "";
if (!subtitle)
return "";
if (!query)
return subtitle;
return root.highlightText(subtitle, query, Theme.surfaceVariantText);
}
textFormat: root.controller?.searchQuery ? Text.RichText : Text.PlainText
text: root.item?._hSub ?? root.item?.subtitle ?? ""
textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
font.pixelSize: Theme.fontSizeSmall
font.family: Theme.fontFamily
color: Theme.surfaceVariantText
@@ -193,7 +164,7 @@ Rectangle {
}
Rectangle {
visible: root.item?.type && root.item.type !== "app" && root.item.type !== "plugin_browse"
visible: !!root.item?.type && root.item.type !== "app" && root.item.type !== "plugin_browse"
width: typeBadge.implicitWidth + Theme.spacingS * 2
height: 20
radius: 10

View File

@@ -11,11 +11,111 @@ Item {
property var controller: null
property int gridColumns: controller?.gridColumns ?? 4
property var _visualRows: []
property var _flatIndexToRowMap: ({})
property var _cumulativeHeights: []
signal itemRightClicked(int index, var item, real mouseX, real mouseY)
function _rebuildVisualModel() {
var sections = root.controller?.sections ?? [];
var rows = [];
var indexMap = {};
var cumHeights = [];
var cumY = 0;
for (var s = 0; s < sections.length; s++) {
var section = sections[s];
var sectionId = section.id;
cumHeights.push(cumY);
rows.push({
_rowId: "h_" + sectionId,
type: "header",
section: section,
sectionId: sectionId,
height: 32
});
cumY += 32;
if (section.collapsed)
continue;
var versionTrigger = root.controller?.viewModeVersion ?? 0;
void (versionTrigger);
var mode = root.controller?.getSectionViewMode(sectionId) ?? "list";
var items = section.items ?? [];
var flatStartIndex = section.flatStartIndex ?? 0;
if (mode === "list") {
for (var i = 0; i < items.length; i++) {
var flatIdx = flatStartIndex + i;
indexMap[flatIdx] = rows.length;
cumHeights.push(cumY);
rows.push({
_rowId: items[i].id,
type: "list_item",
item: items[i],
flatIndex: flatIdx,
sectionId: sectionId,
height: 52
});
cumY += 52;
}
} else {
var cols = root.controller?.getGridColumns(sectionId) ?? root.gridColumns;
var cellWidth = mode === "tile" ? Math.floor(root.width / 3) : Math.floor(root.width / root.gridColumns);
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
var numRows = Math.ceil(items.length / cols);
for (var r = 0; r < numRows; r++) {
var rowItems = [];
for (var c = 0; c < cols; c++) {
var idx = r * cols + c;
if (idx >= items.length)
break;
var fi = flatStartIndex + idx;
indexMap[fi] = rows.length;
rowItems.push({
item: items[idx],
flatIndex: fi
});
}
cumHeights.push(cumY);
rows.push({
_rowId: "gr_" + sectionId + "_" + r,
type: "grid_row",
items: rowItems,
sectionId: sectionId,
viewMode: mode,
cols: cols,
height: cellHeight
});
cumY += cellHeight;
}
}
}
root._flatIndexToRowMap = indexMap;
root._cumulativeHeights = cumHeights;
root._visualRows = rows;
}
onGridColumnsChanged: Qt.callLater(_rebuildVisualModel)
onWidthChanged: Qt.callLater(_rebuildVisualModel)
Connections {
target: root.controller
function onSectionsChanged() {
Qt.callLater(root._rebuildVisualModel);
}
function onViewModeVersionChanged() {
Qt.callLater(root._rebuildVisualModel);
}
}
function resetScroll() {
mainFlickable.contentY = 0;
mainListView.contentY = mainListView.originY;
}
function ensureVisible(index) {
@@ -24,75 +124,10 @@ Item {
var entry = controller.flatModel[index];
if (!entry || entry.isHeader)
return;
scrollItemIntoView(index, entry.sectionId);
}
function scrollItemIntoView(flatIndex, sectionId) {
var sections = controller?.sections ?? [];
var sectionIndex = -1;
for (var i = 0; i < sections.length; i++) {
if (sections[i].id === sectionId) {
sectionIndex = i;
break;
}
}
if (sectionIndex < 0)
var rowIndex = _flatIndexToRowMap[index];
if (rowIndex === undefined)
return;
var itemInSection = 0;
var foundSection = false;
for (var i = 0; i < controller.flatModel.length && i < flatIndex; i++) {
var e = controller.flatModel[i];
if (e.isHeader && e.section?.id === sectionId)
foundSection = true;
else if (foundSection && !e.isHeader && e.sectionId === sectionId)
itemInSection++;
}
var mode = controller.getSectionViewMode(sectionId);
var sectionY = 0;
for (var i = 0; i < sectionIndex; i++) {
sectionY += getSectionHeight(sections[i]);
}
var itemY, itemHeight;
if (mode === "list") {
itemY = itemInSection * 52;
itemHeight = 52;
} else {
var cols = controller.getGridColumns(sectionId);
var cellWidth = mode === "tile" ? Math.floor(mainFlickable.width / 3) : Math.floor(mainFlickable.width / root.gridColumns);
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
var row = Math.floor(itemInSection / cols);
itemY = row * cellHeight;
itemHeight = cellHeight;
}
var targetY = sectionY + 32 + itemY;
var targetBottom = targetY + itemHeight;
var stickyHeight = mainFlickable.contentY > 0 ? 32 : 0;
var shadowPadding = 24;
if (targetY < mainFlickable.contentY + stickyHeight) {
mainFlickable.contentY = Math.max(0, targetY - 32);
} else if (targetBottom > mainFlickable.contentY + mainFlickable.height - shadowPadding) {
mainFlickable.contentY = Math.min(mainFlickable.contentHeight - mainFlickable.height, targetBottom - mainFlickable.height + shadowPadding);
}
}
function getSectionHeight(section) {
var mode = controller?.getSectionViewMode(section.id) ?? "list";
if (section.collapsed)
return 32;
if (mode === "list") {
return 32 + (section.items?.length ?? 0) * 52;
} else {
var cols = controller?.getGridColumns(section.id) ?? root.gridColumns;
var rows = Math.ceil((section.items?.length ?? 0) / cols);
var cellWidth = mode === "tile" ? Math.floor(root.width / 3) : Math.floor(root.width / cols);
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
return 32 + rows * cellHeight;
}
mainListView.positionViewAtIndex(rowIndex, ListView.Contain);
}
function getSelectedItemPosition() {
@@ -104,42 +139,30 @@ Item {
if (!entry || entry.isHeader)
return fallback;
var sections = controller.sections;
var sectionIndex = -1;
for (var i = 0; i < sections.length; i++) {
if (sections[i].id === entry.sectionId) {
sectionIndex = i;
break;
}
}
if (sectionIndex < 0)
var rowIndex = _flatIndexToRowMap[controller.selectedFlatIndex];
if (rowIndex === undefined)
return fallback;
var sectionY = 0;
for (var i = 0; i < sectionIndex; i++) {
sectionY += getSectionHeight(sections[i]);
var rowY = (rowIndex < _cumulativeHeights.length) ? _cumulativeHeights[rowIndex] : 0;
var row = _visualRows[rowIndex];
if (!row)
return fallback;
var itemX = width / 2;
var itemH = row.height;
if (row.type === "grid_row") {
var rowItems = row.items;
for (var i = 0; i < rowItems.length; i++) {
if (rowItems[i].flatIndex === controller.selectedFlatIndex) {
var cellWidth = row.viewMode === "tile" ? Math.floor(width / 3) : Math.floor(width / row.cols);
itemX = i * cellWidth + cellWidth / 2;
break;
}
}
}
var mode = controller.getSectionViewMode(entry.sectionId);
var itemInSection = entry.indexInSection || 0;
var itemY, itemX, itemH;
if (mode === "list") {
itemY = sectionY + 32 + itemInSection * 52;
itemX = width / 2;
itemH = 52;
} else {
var cols = controller.getGridColumns(entry.sectionId);
var cellWidth = mode === "tile" ? Math.floor(width / 3) : Math.floor(width / cols);
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
var row = Math.floor(itemInSection / cols);
var col = itemInSection % cols;
itemY = sectionY + 32 + row * cellHeight;
itemX = col * cellWidth + cellWidth / 2;
itemH = cellHeight;
}
var visualY = itemY - mainFlickable.contentY + itemH / 2;
var visualY = rowY - mainListView.contentY + mainListView.originY + itemH / 2;
var clampedY = Math.max(40, Math.min(height - 40, visualY));
return mapToItem(null, itemX, clampedY);
}
@@ -153,161 +176,124 @@ Item {
}
}
DankFlickable {
id: mainFlickable
DankListView {
id: mainListView
anchors.fill: parent
contentWidth: width
contentHeight: sectionsColumn.height
clip: true
scrollBarTopMargin: (root.controller?.sections?.length > 0) ? 32 : 0
Component.onCompleted: {
verticalScrollBar.targetFlickable = mainFlickable;
verticalScrollBar.parent = root;
verticalScrollBar.z = 102;
verticalScrollBar.anchors.right = root.right;
verticalScrollBar.anchors.top = root.top;
verticalScrollBar.anchors.bottom = root.bottom;
model: ScriptModel {
values: root._visualRows
objectProp: "_rowId"
}
Column {
id: sectionsColumn
width: parent.width
add: null
remove: null
displaced: null
move: null
Repeater {
model: ScriptModel {
values: root.controller?.sections ?? []
objectProp: "id"
delegate: Item {
id: delegateRoot
required property var modelData
required property int index
width: mainListView.width
height: modelData?.height ?? 52
SectionHeader {
anchors.fill: parent
visible: delegateRoot.modelData?.type === "header"
section: delegateRoot.modelData?.section ?? null
controller: root.controller
viewMode: {
var vt = root.controller?.viewModeVersion ?? 0;
void (vt);
return root.controller?.getSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? "list";
}
canChangeViewMode: {
var vt = root.controller?.viewModeVersion ?? 0;
void (vt);
return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false;
}
canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
}
ResultItem {
anchors.fill: parent
visible: delegateRoot.modelData?.type === "list_item"
item: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.item ?? null) : null
isSelected: delegateRoot.modelData?.type === "list_item" && (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.flatIndex ?? -1) : -1
onClicked: {
if (root.controller && delegateRoot.modelData?.item) {
root.controller.executeItem(delegateRoot.modelData.item);
}
}
Column {
id: sectionDelegate
required property var modelData
required property int index
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY);
}
}
readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0
readonly property string sectionId: modelData?.id ?? ""
readonly property string currentViewMode: {
void (versionTrigger);
return root.controller?.getSectionViewMode(sectionId) ?? "list";
}
readonly property bool isGridMode: currentViewMode === "grid" || currentViewMode === "tile"
readonly property bool isCollapsed: modelData?.collapsed ?? false
Row {
id: gridRowContent
anchors.fill: parent
visible: delegateRoot.modelData?.type === "grid_row"
width: sectionsColumn.width
Repeater {
model: delegateRoot.modelData?.type === "grid_row" ? (delegateRoot.modelData?.items ?? []) : []
SectionHeader {
width: parent.width
height: 32
section: sectionDelegate.modelData
controller: root.controller
viewMode: sectionDelegate.currentViewMode
canChangeViewMode: root.controller?.canChangeSectionViewMode(sectionDelegate.sectionId) ?? false
canCollapse: root.controller?.canCollapseSection(sectionDelegate.sectionId) ?? false
}
Item {
id: gridCellDelegate
required property var modelData
required property int index
Column {
id: listContent
width: parent.width
visible: !sectionDelegate.isGridMode && !sectionDelegate.isCollapsed
readonly property real cellWidth: delegateRoot.modelData?.viewMode === "tile" ? Math.floor(delegateRoot.width / 3) : Math.floor(delegateRoot.width / (delegateRoot.modelData?.cols ?? root.gridColumns))
Repeater {
model: ScriptModel {
values: sectionDelegate.isGridMode || sectionDelegate.isCollapsed ? [] : (sectionDelegate.modelData?.items ?? [])
objectProp: "id"
width: cellWidth
height: delegateRoot.height
GridItem {
width: parent.width - 4
height: parent.height - 4
anchors.centerIn: parent
visible: delegateRoot.modelData?.viewMode === "grid"
item: gridCellDelegate.modelData?.item ?? null
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
onClicked: {
if (root.controller && gridCellDelegate.modelData?.item) {
root.controller.executeItem(gridCellDelegate.modelData.item);
}
}
ResultItem {
required property var modelData
required property int index
readonly property int computedFlatIndex: (sectionDelegate.modelData?.flatStartIndex ?? 0) + index
width: listContent.width
height: 52
item: modelData
isSelected: computedFlatIndex === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: computedFlatIndex
onClicked: {
if (root.controller) {
root.controller.executeItem(modelData);
}
}
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(computedFlatIndex, modelData, mouseX, mouseY);
}
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
}
}
}
Grid {
id: gridContent
width: parent.width
visible: sectionDelegate.isGridMode && !sectionDelegate.isCollapsed
columns: sectionDelegate.currentViewMode === "tile" ? 3 : root.gridColumns
TileItem {
width: parent.width - 4
height: parent.height - 4
anchors.centerIn: parent
visible: delegateRoot.modelData?.viewMode === "tile"
item: gridCellDelegate.modelData?.item ?? null
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
readonly property real cellWidth: sectionDelegate.currentViewMode === "tile" ? Math.floor(width / 3) : Math.floor(width / root.gridColumns)
readonly property real cellHeight: sectionDelegate.currentViewMode === "tile" ? cellWidth * 0.75 : cellWidth + 24
Repeater {
model: ScriptModel {
values: sectionDelegate.isGridMode && !sectionDelegate.isCollapsed ? (sectionDelegate.modelData?.items ?? []) : []
objectProp: "id"
onClicked: {
if (root.controller && gridCellDelegate.modelData?.item) {
root.controller.executeItem(gridCellDelegate.modelData.item);
}
}
Item {
id: gridDelegateItem
required property var modelData
required property int index
width: gridContent.cellWidth
height: gridContent.cellHeight
readonly property int cachedFlatIndex: (sectionDelegate.modelData?.flatStartIndex ?? 0) + index
GridItem {
width: parent.width - 4
height: parent.height - 4
anchors.centerIn: parent
visible: sectionDelegate.currentViewMode === "grid"
item: gridDelegateItem.modelData
isSelected: gridDelegateItem.cachedFlatIndex === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridDelegateItem.cachedFlatIndex
onClicked: {
if (root.controller) {
root.controller.executeItem(gridDelegateItem.modelData);
}
}
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(gridDelegateItem.cachedFlatIndex, gridDelegateItem.modelData, mouseX, mouseY);
}
}
TileItem {
width: parent.width - 4
height: parent.height - 4
anchors.centerIn: parent
visible: sectionDelegate.currentViewMode === "tile"
item: gridDelegateItem.modelData
isSelected: gridDelegateItem.cachedFlatIndex === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridDelegateItem.cachedFlatIndex
onClicked: {
if (root.controller) {
root.controller.executeItem(gridDelegateItem.modelData);
}
}
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(gridDelegateItem.cachedFlatIndex, gridDelegateItem.modelData, mouseX, mouseY);
}
}
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
}
}
}
@@ -324,9 +310,9 @@ Item {
height: 24
z: 100
visible: {
if (mainFlickable.contentHeight <= mainFlickable.height)
if (mainListView.contentHeight <= mainListView.height)
return false;
var atBottom = mainFlickable.contentY >= mainFlickable.contentHeight - mainFlickable.height - 5;
var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5;
if (atBottom)
return false;
@@ -371,23 +357,31 @@ Item {
readonly property var stickyHeaderSection: {
if (!root.controller?.sections || root.controller.sections.length === 0)
return null;
var sections = root.controller.sections;
if (sections.length === 0)
return null;
var scrollY = mainFlickable.contentY;
var scrollY = mainListView.contentY - mainListView.originY;
if (scrollY <= 0)
return null;
var y = 0;
for (var i = 0; i < sections.length; i++) {
var section = sections[i];
var sectionHeight = root.getSectionHeight(section);
if (scrollY < y + sectionHeight)
return section;
y += sectionHeight;
var rows = root._visualRows;
var heights = root._cumulativeHeights;
if (rows.length === 0 || heights.length === 0)
return null;
var lo = 0;
var hi = rows.length - 1;
while (lo < hi) {
var mid = (lo + hi + 1) >> 1;
if (mid < heights.length && heights[mid] <= scrollY)
lo = mid;
else
hi = mid - 1;
}
return sections[sections.length - 1];
for (var i = lo; i >= 0; i--) {
if (rows[i].type === "header")
return rows[i].section;
}
return null;
}
SectionHeader {

View File

@@ -187,7 +187,8 @@ function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSec
icon: sectionOrder[i].icon,
priority: sectionOrder[i].priority,
items: [],
collapsed: false
collapsed: false,
flatStartIndex: 0
}
}
@@ -220,6 +221,8 @@ function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSec
function flattenSections(sections) {
var flat = []
flat._sectionBounds = null
var bounds = {}
for (var i = 0; i < sections.length; i++) {
var section = sections[i]
@@ -231,7 +234,8 @@ function flattenSections(sections) {
sectionIndex: i
})
section.flatStartIndex = flat.length
var itemStart = flat.length
section.flatStartIndex = itemStart
if (!section.collapsed) {
for (var j = 0; j < section.items.length; j++) {
@@ -244,7 +248,18 @@ function flattenSections(sections) {
})
}
}
var itemEnd = flat.length - 1
var itemCount = flat.length - itemStart
if (itemCount > 0) {
bounds[section.id] = {
start: itemStart,
end: itemEnd,
count: itemCount
}
}
}
flat._sectionBounds = bounds
return flat
}

View File

@@ -23,21 +23,6 @@ Rectangle {
border.width: isSelected ? 2 : 0
border.color: Theme.primary
function highlightText(text, query, baseColor) {
if (!text || !query || query.length === 0)
return text;
var lowerText = text.toLowerCase();
var lowerQuery = query.toLowerCase();
var idx = lowerText.indexOf(lowerQuery);
if (idx === -1)
return text;
var before = text.substring(0, idx);
var match = text.substring(idx, idx + query.length);
var after = text.substring(idx + query.length);
var highlightColor = Theme.primary;
return '<span style="color:' + baseColor + '">' + before + '</span>' + '<span style="color:' + highlightColor + '; font-weight:600">' + match + '</span>' + '<span style="color:' + baseColor + '">' + after + '</span>';
}
readonly property string toplevelId: item?.data?.toplevelId ?? ""
readonly property var waylandToplevel: {
if (!toplevelId || !item?.pluginId)
@@ -133,19 +118,13 @@ Rectangle {
id: labelText
anchors.fill: parent
anchors.margins: Theme.spacingXS
text: {
var query = root.controller?.searchQuery ?? "";
var name = root.item?.name ?? "";
if (!query)
return name;
return root.highlightText(name, query, Theme.surfaceText);
}
textFormat: root.controller?.searchQuery ? Text.RichText : Text.PlainText
text: root.item?._hName ?? root.item?.name ?? ""
textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
font.pixelSize: Theme.fontSizeSmall
font.family: Theme.fontFamily
color: Theme.surfaceText
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
}
}

View File

@@ -21,6 +21,8 @@ DankModal {
property var historyListRef: null
property int currentTab: 0
property var notificationHeaderRef: null
function show() {
notificationModalOpen = true;
currentTab = 0;
@@ -89,6 +91,22 @@ DankModal {
event.accepted = true;
return;
}
if (event.key === Qt.Key_Left) {
if (notificationHeaderRef && notificationHeaderRef.currentTab > 0) {
notificationHeaderRef.currentTab = 0;
event.accepted = true;
}
return;
}
if (event.key === Qt.Key_Right) {
if (notificationHeaderRef && notificationHeaderRef.currentTab === 0 && SettingsData.notificationHistoryEnabled) {
notificationHeaderRef.currentTab = 1;
event.accepted = true;
}
return;
}
if (currentTab === 1 && historyListRef) {
historyListRef.handleKey(event);
return;
@@ -161,6 +179,7 @@ DankModal {
id: notificationHeader
keyboardController: modalKeyboardController
onCurrentTabChanged: notificationModal.currentTab = currentTab
Component.onCompleted: notificationModal.notificationHeaderRef = notificationHeader
}
NotificationSettings {

View File

@@ -180,6 +180,7 @@ Variants {
}
// Hyprland implementation
Hyprland.focusedWorkspace;
const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
if (filtered.length === 0)
@@ -381,9 +382,7 @@ Variants {
const globalX = buttonGlobalPos.x + dock.hoveredButton.width / 2 + adjacentLeftBarWidth;
const tooltipHeight = 32;
const tooltipOffset = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + barSpacing + Theme.spacingM;
const screenRelativeY = isBottom
? (screenHeight - tooltipOffset - tooltipHeight)
: tooltipOffset;
const screenRelativeY = isBottom ? (screenHeight - tooltipOffset - tooltipHeight) : tooltipOffset;
dockTooltip.show(tooltipText, globalX, screenRelativeY, dock.screen, false, false);
return;
}

View File

@@ -15,7 +15,8 @@ Rectangle {
Component.onCompleted: {
Qt.callLater(() => {
__initialized = true;
if (root)
root.__initialized = true;
});
}

View File

@@ -257,7 +257,8 @@ Item {
Component.onCompleted: {
Qt.callLater(() => {
__delegateInitialized = true;
if (delegateRoot)
delegateRoot.__delegateInitialized = true;
});
}

View File

@@ -149,6 +149,22 @@ DankPopout {
event.accepted = true;
return;
}
if (event.key === Qt.Key_Left) {
if (notificationHeader.currentTab > 0) {
notificationHeader.currentTab = 0;
event.accepted = true;
}
return;
}
if (event.key === Qt.Key_Right) {
if (notificationHeader.currentTab === 0 && SettingsData.notificationHistoryEnabled) {
notificationHeader.currentTab = 1;
event.accepted = true;
}
return;
}
if (notificationHeader.currentTab === 1) {
historyList.handleKey(event);
return;

View File

@@ -1,5 +1,4 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import qs.Common
import qs.Services
@@ -37,9 +36,23 @@ DankOSD {
}
}
property bool _pendingShow: false
onPlayerChanged: {
if (!player)
if (!player) {
_pendingShow = false;
hide();
}
}
Connections {
target: TrackArtService
function onLoadingChanged() {
if (!TrackArtService.loading && root._pendingShow) {
root._pendingShow = false;
root.show();
}
}
}
Connections {
@@ -48,10 +61,20 @@ DankOSD {
function handleUpdate() {
if (!root.player?.trackTitle)
return;
if (SettingsData.osdMediaPlaybackEnabled) {
TrackArtService.loadArtwork(player.trackArtUrl);
if (!SettingsData.osdMediaPlaybackEnabled)
return;
TrackArtService.loadArtwork(player.trackArtUrl);
if (!player.trackArtUrl || player.trackArtUrl === "") {
root.show();
return;
}
if (!TrackArtService.loading) {
root.show();
return;
}
root._pendingShow = true;
}
function onTrackArtUrlChanged() {

View File

@@ -368,18 +368,26 @@ Singleton {
let currentWorkspaceId = null;
try {
const hy = Array.from(Hyprland.toplevels.values);
for (const t of hy) {
const mon = _get(t, ["monitor", "name"], "");
const wsId = _get(t, ["workspace", "id"], null);
const active = !!_get(t, ["activated"], false);
if (mon === screenName && wsId !== null) {
if (active) {
currentWorkspaceId = wsId;
break;
if (Hyprland.monitors) {
const monitor = Hyprland.monitors.values.find(m => m.name === screenName);
if (monitor)
currentWorkspaceId = _get(monitor, ["activeWorkspace", "id"], null);
}
if (currentWorkspaceId === null) {
const hy = Array.from(Hyprland.toplevels.values);
for (const t of hy) {
const mon = _get(t, ["monitor", "name"], "");
const wsId = _get(t, ["workspace", "id"], null);
const active = !!_get(t, ["activated"], false);
if (mon === screenName && wsId !== null) {
if (active) {
currentWorkspaceId = wsId;
break;
}
if (currentWorkspaceId === null)
currentWorkspaceId = wsId;
}
if (currentWorkspaceId === null)
currentWorkspaceId = wsId;
}
}
@@ -387,7 +395,7 @@ Singleton {
const wss = Array.from(Hyprland.workspaces.values);
const focusedId = _get(Hyprland, ["focusedWorkspace", "id"], null);
for (const ws of wss) {
const monName = _get(ws, ["monitor"], "");
const monName = _get(ws, ["monitor", "name"], "");
const wsId = _get(ws, ["id"], null);
if (monName === screenName && wsId !== null) {
if (focusedId !== null && wsId === focusedId) {
@@ -406,7 +414,6 @@ Singleton {
if (currentWorkspaceId === null)
return toplevels;
// Map wayland → wsId snapshot
let map = new Map();
try {
const hy = Array.from(Hyprland.toplevels.values);

View File

@@ -3,62 +3,62 @@ pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import Quickshell.Io
import Quickshell.Services.Mpris
import qs.Common
Singleton {
id: root
property string _lastArtUrl: ""
property string _bgArtSource: ""
property string activeTrackArtFile: ""
property bool loading: false
function loadArtwork(url) {
if (!url || url == "") {
if (!url || url === "") {
_bgArtSource = "";
_lastArtUrl = "";
loading = false;
return;
}
if (url == _lastArtUrl)
if (url === _lastArtUrl)
return;
_lastArtUrl = url;
if (url.startsWith("http://") || url.startsWith("https://")) {
const filename = "/tmp/.dankshell/trackart_" + Date.now() + ".jpg";
activeTrackArtFile = filename;
cleanupProcess.command = ["sh", "-c", "mkdir -p /tmp/.dankshell && find /tmp/.dankshell -name 'trackart_*' ! -name '" + filename.split('/').pop() + "' -delete"];
cleanupProcess.running = true;
_bgArtSource = "";
loading = true;
imageDownloader.command = ["curl", "-L", "-s", "--user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", "-o", filename, url];
imageDownloader.targetFile = filename;
imageDownloader.running = true;
if (!url.startsWith("http://") && !url.startsWith("https://")) {
const localUrl = url;
const filePath = url.startsWith("file://") ? url.substring(7) : url;
Proc.runCommand("trackart", ["test", "-f", filePath], (output, exitCode) => {
if (_lastArtUrl !== localUrl)
return;
if (exitCode === 0)
_bgArtSource = localUrl;
loading = false;
}, 200);
return;
}
// otherwise
_bgArtSource = url;
const filename = "/tmp/.dankshell/trackart_" + Date.now() + ".jpg";
activeTrackArtFile = filename;
Proc.runCommand("trackart_cleanup", ["sh", "-c", "mkdir -p /tmp/.dankshell && find /tmp/.dankshell -name 'trackart_*' ! -name '" + filename.split('/').pop() + "' -delete"], null, 0);
Proc.runCommand("trackart", ["dms", "dl", "-o", filename, "--user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", url], (output, exitCode) => {
const resultPath = output.trim();
if (resultPath !== filename)
return;
if (exitCode === 0)
_bgArtSource = "file://" + resultPath;
loading = false;
}, 200);
}
property MprisPlayer activePlayer: MprisController.activePlayer
onActivePlayerChanged: {
loadArtwork(activePlayer.trackArtUrl);
}
Process {
id: imageDownloader
running: false
property string targetFile: ""
onExited: exitCode => {
if (exitCode === 0 && targetFile)
_bgArtSource = "file://" + targetFile;
}
}
Process {
id: cleanupProcess
running: false
loadArtwork(activePlayer?.trackArtUrl ?? "");
}
}

View File

@@ -6,6 +6,7 @@ import qs.Widgets
ListView {
id: listView
property real scrollBarTopMargin: 0
property real mouseWheelSpeed: 60
property real savedY: 0
property bool justChanged: false
@@ -208,5 +209,6 @@ ListView {
ScrollBar.vertical: DankScrollbar {
id: vbar
topPadding: listView.scrollBarTopMargin
}
}

View File

@@ -1,5 +1,4 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
@@ -13,7 +12,7 @@ Item {
onActiveFocusChanged: {
if (activeFocus) {
locationInput.forceActiveFocus()
locationInput.forceActiveFocus();
}
}
@@ -28,10 +27,10 @@ Item {
signal locationSelected(string displayName, string coordinates)
function resetSearchState() {
locationSearchTimer.stop()
dropdownHideTimer.stop()
isLoading = false
searchResultsModel.clear()
locationSearchTimer.stop();
dropdownHideTimer.stop();
isLoading = false;
searchResultsModel.clear();
}
width: parent.width
@@ -49,52 +48,49 @@ Item {
repeat: false
onTriggered: {
if (locationInput.text.length > 2) {
searchResultsModel.clear()
root.isLoading = true
const searchLocation = locationInput.text
root.currentSearchText = searchLocation
const encodedLocation = encodeURIComponent(searchLocation)
const curlCommand = `curl -4 -s --connect-timeout 5 --max-time 10 'https://nominatim.openstreetmap.org/search?q=${encodedLocation}&format=json&limit=5&addressdetails=1'`
Proc.runCommand("locationSearch", ["bash", "-c", curlCommand], (output, exitCode) => {
root.isLoading = false
searchResultsModel.clear();
root.isLoading = true;
const searchLocation = locationInput.text;
root.currentSearchText = searchLocation;
const encodedLocation = encodeURIComponent(searchLocation);
const searchUrl = "https://nominatim.openstreetmap.org/search?q=" + encodedLocation + "&format=json&limit=5&addressdetails=1";
Proc.runCommand("locationSearch", ["dms", "dl", "-4", "--timeout", "10", searchUrl], (output, exitCode) => {
root.isLoading = false;
if (exitCode !== 0) {
searchResultsModel.clear()
return
searchResultsModel.clear();
return;
}
if (root.currentSearchText !== locationInput.text)
return
const raw = output.trim()
searchResultsModel.clear()
return;
const raw = output.trim();
searchResultsModel.clear();
if (!raw || raw[0] !== "[") {
return
return;
}
try {
const data = JSON.parse(raw)
const data = JSON.parse(raw);
if (data.length === 0) {
return
return;
}
for (var i = 0; i < Math.min(data.length, 5); i++) {
const location = data[i]
const location = data[i];
if (location.display_name && location.lat && location.lon) {
const parts = location.display_name.split(', ')
let cleanName = parts[0]
const parts = location.display_name.split(', ');
let cleanName = parts[0];
if (parts.length > 1) {
const state = parts[parts.length - 2]
const state = parts[parts.length - 2];
if (state && state !== cleanName)
cleanName += `, ${state}`
cleanName += `, ${state}`;
}
const query = `${location.lat},${location.lon}`
const query = `${location.lat},${location.lon}`;
searchResultsModel.append({
"name": cleanName,
"query": query
})
"name": cleanName,
"query": query
});
}
}
} catch (e) {
}
})
} catch (e) {}
});
}
}
}
@@ -107,7 +103,7 @@ Item {
repeat: false
onTriggered: {
if (!locationInput.getActiveFocus() && !searchDropdown.hovered)
root.resetSearchState()
root.resetSearchState();
}
}
@@ -132,23 +128,23 @@ Item {
keyNavigationBacktab: root.keyNavigationBacktab
onTextEdited: {
if (root._internalChange)
return
return;
if (getActiveFocus()) {
if (text.length > 2) {
root.isLoading = true
locationSearchTimer.restart()
root.isLoading = true;
locationSearchTimer.restart();
} else {
root.resetSearchState()
root.resetSearchState();
}
}
}
onFocusStateChanged: hasFocus => {
if (hasFocus) {
dropdownHideTimer.stop()
} else {
dropdownHideTimer.start()
}
}
if (hasFocus) {
dropdownHideTimer.stop();
} else {
dropdownHideTimer.start();
}
}
}
DankIcon {
@@ -187,13 +183,13 @@ Item {
anchors.fill: parent
hoverEnabled: true
onEntered: {
parent.hovered = true
dropdownHideTimer.stop()
parent.hovered = true;
dropdownHideTimer.stop();
}
onExited: {
parent.hovered = false
parent.hovered = false;
if (!locationInput.getActiveFocus())
dropdownHideTimer.start()
dropdownHideTimer.start();
}
acceptedButtons: Qt.NoButton
}
@@ -245,14 +241,14 @@ Item {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root._internalChange = true
const selectedName = model.name
const selectedQuery = model.query
locationInput.text = selectedName
root.locationSelected(selectedName, selectedQuery)
root.resetSearchState()
locationInput.setFocus(false)
root._internalChange = false
root._internalChange = true;
const selectedName = model.name;
const selectedQuery = model.query;
locationInput.text = selectedName;
root.locationSelected(selectedName, selectedQuery);
root.resetSearchState();
locationInput.setFocus(false);
root._internalChange = false;
}
}
}