mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-03 19:12:11 -04:00
Compare commits
10 Commits
46a2f6f0d8
...
3c2d60d8e1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c2d60d8e1 | ||
|
|
9c4f4cbd0d | ||
|
|
a337585b00 | ||
|
|
1cdec5d687 | ||
|
|
081b15e24c | ||
|
|
b04cb7b3cc | ||
|
|
e2c3ff00fb | ||
|
|
c783ff3dcf | ||
|
|
2c360dc3e8 | ||
|
|
5342647bfb |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -524,5 +524,6 @@ func getCommonCommands() []*cobra.Command {
|
||||
chromaCmd,
|
||||
doctorCmd,
|
||||
configCmd,
|
||||
dlCmd,
|
||||
}
|
||||
}
|
||||
|
||||
99
core/cmd/dms/commands_download.go
Normal file
99
core/cmd/dms/commands_download.go
Normal 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
|
||||
}
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
31
core/internal/utils/gsettings.go
Normal file
31
core/internal/utils/gsettings.go
Normal 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()
|
||||
}
|
||||
@@ -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 => {}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ FocusScope {
|
||||
|
||||
Controller {
|
||||
id: controller
|
||||
active: root.parentModal?.spotlightOpen ?? true
|
||||
viewModeContext: root.viewModeContext
|
||||
|
||||
onItemExecuted: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ Rectangle {
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(() => {
|
||||
__initialized = true;
|
||||
if (root)
|
||||
root.__initialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -257,7 +257,8 @@ Item {
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(() => {
|
||||
__delegateInitialized = true;
|
||||
if (delegateRoot)
|
||||
delegateRoot.__delegateInitialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user