mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-15 15:45:20 -04:00
Compare commits
5 Commits
cd672c341f
...
08fd6e26d8
| Author | SHA1 | Date | |
|---|---|---|---|
| 08fd6e26d8 | |||
| 29e8470f2e | |||
| 573785d4ce | |||
| 5483303714 | |||
| 5a5cc4f4e9 |
@@ -115,3 +115,5 @@ core.*
|
||||
.direnv/
|
||||
quickshell/dms-plugins
|
||||
__pycache__
|
||||
|
||||
.vscode/
|
||||
|
||||
@@ -935,7 +935,7 @@ func (m *Manager) CreateHistoryEntryFromPinned(pinnedEntry *Entry) error {
|
||||
Pinned: false,
|
||||
}
|
||||
|
||||
if err := m.storeEntryWithoutDedup(newEntry); err != nil {
|
||||
if err := m.storeEntry(newEntry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -945,36 +945,6 @@ func (m *Manager) CreateHistoryEntryFromPinned(pinnedEntry *Entry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) storeEntryWithoutDedup(entry Entry) error {
|
||||
if m.db == nil {
|
||||
return fmt.Errorf("database not available")
|
||||
}
|
||||
|
||||
entry.Hash = computeHash(entry.Data)
|
||||
|
||||
return m.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
|
||||
id, err := b.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entry.ID = id
|
||||
|
||||
encoded, err := encodeEntry(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.Put(itob(id), encoded); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.trimLengthInTx(b)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) ClearHistory() {
|
||||
if m.db == nil {
|
||||
return
|
||||
@@ -1653,6 +1623,37 @@ func (m *Manager) UnpinEntry(id uint64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if entry.Pinned {
|
||||
currentKey := itob(id)
|
||||
var keepKey []byte
|
||||
var deleteKeys [][]byte
|
||||
|
||||
c := b.Cursor()
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
if bytes.Equal(k, currentKey) || extractHash(v) != entry.Hash {
|
||||
continue
|
||||
}
|
||||
duplicate, err := decodeEntryMeta(v)
|
||||
if err == nil && !duplicate.Pinned {
|
||||
key := append([]byte(nil), k...)
|
||||
if keepKey == nil {
|
||||
keepKey = key
|
||||
} else {
|
||||
deleteKeys = append(deleteKeys, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if keepKey != nil {
|
||||
for _, key := range deleteKeys {
|
||||
if err := b.Delete(key); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return b.Delete(currentKey)
|
||||
}
|
||||
}
|
||||
|
||||
entry.Pinned = false
|
||||
encoded, err := encodeEntry(entry)
|
||||
if err != nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
|
||||
mocks_wlcontext "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlcontext"
|
||||
)
|
||||
@@ -273,6 +274,110 @@ func TestHandleGetEntry_MissingIDReturnsNullResult(t *testing.T) {
|
||||
assert.Nil(t, resp.Result)
|
||||
}
|
||||
|
||||
func TestUnpinEntry_KeepsTopUnpinnedDuplicate(t *testing.T) {
|
||||
m := newTestManagerWithDB(t)
|
||||
|
||||
require.NoError(t, m.storeEntry(Entry{
|
||||
Data: []byte("saved content"),
|
||||
MimeType: "text/plain;charset=utf-8",
|
||||
Preview: "saved content",
|
||||
Size: len("saved content"),
|
||||
Timestamp: time.Now().Add(-time.Minute).Truncate(time.Second),
|
||||
IsImage: false,
|
||||
}))
|
||||
|
||||
history := m.GetHistory()
|
||||
require.Len(t, history, 1)
|
||||
pinnedID := history[0].ID
|
||||
require.NoError(t, m.PinEntry(pinnedID))
|
||||
|
||||
pinnedEntry, err := m.GetEntry(pinnedID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, pinnedEntry.Pinned)
|
||||
|
||||
// Bypass storeEntry to simulate legacy duplicate ordinary history entries.
|
||||
insertLegacyUnpinnedDuplicate := func(timestamp time.Time) Entry {
|
||||
duplicate := Entry{
|
||||
Data: pinnedEntry.Data,
|
||||
MimeType: pinnedEntry.MimeType,
|
||||
Preview: pinnedEntry.Preview,
|
||||
Size: pinnedEntry.Size,
|
||||
Timestamp: timestamp,
|
||||
IsImage: pinnedEntry.IsImage,
|
||||
Pinned: false,
|
||||
}
|
||||
duplicate.Hash = computeHash(duplicate.Data)
|
||||
|
||||
require.NoError(t, m.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
id, err := b.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
duplicate.ID = id
|
||||
|
||||
encoded, err := encodeEntry(duplicate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put(itob(id), encoded)
|
||||
}))
|
||||
|
||||
return duplicate
|
||||
}
|
||||
|
||||
olderHistoryDuplicate := insertLegacyUnpinnedDuplicate(time.Now().Add(time.Hour))
|
||||
topHistoryDuplicate := insertLegacyUnpinnedDuplicate(time.Now().Add(-time.Hour))
|
||||
require.Greater(t, topHistoryDuplicate.ID, olderHistoryDuplicate.ID)
|
||||
require.True(t, olderHistoryDuplicate.Timestamp.After(topHistoryDuplicate.Timestamp))
|
||||
|
||||
history = m.GetHistory()
|
||||
require.Len(t, history, 3)
|
||||
require.Equal(t, topHistoryDuplicate.ID, history[0].ID)
|
||||
require.NoError(t, m.UnpinEntry(pinnedID))
|
||||
|
||||
history = m.GetHistory()
|
||||
require.Len(t, history, 1)
|
||||
assert.False(t, history[0].Pinned)
|
||||
assert.Equal(t, pinnedEntry.Hash, history[0].Hash)
|
||||
assert.Equal(t, topHistoryDuplicate.ID, history[0].ID)
|
||||
}
|
||||
|
||||
func TestCreateHistoryEntryFromPinned_KeepsLatestUnpinnedDuplicate(t *testing.T) {
|
||||
m := newTestManagerWithDB(t)
|
||||
|
||||
require.NoError(t, m.storeEntry(Entry{
|
||||
Data: []byte("saved content"),
|
||||
MimeType: "text/plain;charset=utf-8",
|
||||
Preview: "saved content",
|
||||
Size: len("saved content"),
|
||||
Timestamp: time.Now().Add(-time.Minute).Truncate(time.Second),
|
||||
IsImage: false,
|
||||
}))
|
||||
|
||||
history := m.GetHistory()
|
||||
require.Len(t, history, 1)
|
||||
pinnedID := history[0].ID
|
||||
require.NoError(t, m.PinEntry(pinnedID))
|
||||
|
||||
pinnedEntry, err := m.GetEntry(pinnedID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, pinnedEntry.Pinned)
|
||||
require.NoError(t, m.CreateHistoryEntryFromPinned(pinnedEntry))
|
||||
firstDuplicate := m.GetHistory()[0]
|
||||
require.NotEqual(t, pinnedID, firstDuplicate.ID)
|
||||
require.NoError(t, m.CreateHistoryEntryFromPinned(pinnedEntry))
|
||||
latestDuplicate := m.GetHistory()[0]
|
||||
|
||||
history = m.GetHistory()
|
||||
require.Len(t, history, 2)
|
||||
assert.Equal(t, latestDuplicate.ID, history[0].ID)
|
||||
assert.False(t, history[0].Pinned)
|
||||
assert.Equal(t, pinnedID, history[1].ID)
|
||||
assert.True(t, history[1].Pinned)
|
||||
assert.NotEqual(t, firstDuplicate.ID, latestDuplicate.ID)
|
||||
}
|
||||
|
||||
func TestManager_ConcurrentSubscriberAccess(t *testing.T) {
|
||||
m := &Manager{
|
||||
subscribers: make(map[string]chan State),
|
||||
|
||||
@@ -515,6 +515,8 @@ Singleton {
|
||||
property bool notepadUseMonospace: true
|
||||
property string notepadFontFamily: ""
|
||||
property real notepadFontSize: 14
|
||||
property real notificationSummaryFontSize: Spec.SPEC.notificationSummaryFontSize.def
|
||||
property real notificationBodyFontSize: Spec.SPEC.notificationBodyFontSize.def
|
||||
property bool notepadShowLineNumbers: false
|
||||
property real notepadTransparencyOverride: -1
|
||||
property real notepadLastCustomTransparency: 0.7
|
||||
@@ -695,6 +697,7 @@ Singleton {
|
||||
property int notificationTimeoutNormal: 5000
|
||||
property int notificationTimeoutCritical: 0
|
||||
property bool notificationCompactMode: false
|
||||
property bool notificationShowTimeoutBar: false
|
||||
property bool notificationDedupeEnabled: true
|
||||
property int notificationPopupPosition: SettingsData.Position.Top
|
||||
property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short
|
||||
|
||||
@@ -260,6 +260,8 @@ var SPEC = {
|
||||
notepadUseMonospace: { def: true },
|
||||
notepadFontFamily: { def: "" },
|
||||
notepadFontSize: { def: 14 },
|
||||
notificationSummaryFontSize: { def: 0 },
|
||||
notificationBodyFontSize: { def: 0 },
|
||||
notepadShowLineNumbers: { def: false },
|
||||
notepadTransparencyOverride: { def: -1 },
|
||||
notepadLastCustomTransparency: { def: 0.7 },
|
||||
@@ -406,6 +408,7 @@ var SPEC = {
|
||||
notificationTimeoutNormal: { def: 5000 },
|
||||
notificationTimeoutCritical: { def: 0 },
|
||||
notificationCompactMode: { def: false },
|
||||
notificationShowTimeoutBar: { def: false },
|
||||
notificationDedupeEnabled: { def: true },
|
||||
notificationPopupPosition: { def: 0 },
|
||||
notificationAnimationSpeed: { def: 1 },
|
||||
|
||||
@@ -149,8 +149,8 @@ Item {
|
||||
listView: clipboardListView
|
||||
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
||||
onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
|
||||
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
||||
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
||||
onPinRequested: targetEntry => clipboardContent.modal.pinEntry(targetEntry)
|
||||
onUnpinRequested: targetEntry => clipboardContent.modal.unpinEntry(targetEntry)
|
||||
onEditRequested: clipboardContent.modal.editEntry(modelData)
|
||||
}
|
||||
}
|
||||
@@ -223,8 +223,8 @@ Item {
|
||||
listView: savedListView
|
||||
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
||||
onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
|
||||
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
||||
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
||||
onPinRequested: targetEntry => clipboardContent.modal.pinEntry(targetEntry)
|
||||
onUnpinRequested: targetEntry => clipboardContent.modal.unpinEntry(targetEntry)
|
||||
onEditRequested: clipboardContent.modal.editEntry(modelData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,14 @@ Rectangle {
|
||||
|
||||
signal copyRequested
|
||||
signal deleteRequested
|
||||
signal pinRequested
|
||||
signal unpinRequested
|
||||
signal pinRequested(var targetEntry)
|
||||
signal unpinRequested(var targetEntry)
|
||||
signal editRequested
|
||||
|
||||
readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
|
||||
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
|
||||
readonly property bool hasPinnedDuplicate: !entry.pinned && ClipboardService.hashedPinnedEntry(entry.hash)
|
||||
readonly property var pinnedDuplicateEntry: !entry.pinned ? ClipboardService.getPinnedEntryByHash(entry.hash) : null
|
||||
readonly property bool effectivePinned: entry.pinned || pinnedDuplicateEntry !== null
|
||||
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
@@ -66,9 +67,19 @@ Rectangle {
|
||||
DankActionButton {
|
||||
iconName: "push_pin"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primary : Theme.surfaceText
|
||||
backgroundColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primarySelected : "transparent"
|
||||
onClicked: entry.pinned ? unpinRequested() : pinRequested()
|
||||
iconColor: effectivePinned ? Theme.primary : Theme.surfaceText
|
||||
backgroundColor: effectivePinned ? Theme.primarySelected : "transparent"
|
||||
onClicked: {
|
||||
if (entry.pinned) {
|
||||
unpinRequested(entry);
|
||||
return;
|
||||
}
|
||||
if (pinnedDuplicateEntry) {
|
||||
unpinRequested(pinnedDuplicateEntry);
|
||||
return;
|
||||
}
|
||||
pinRequested(entry);
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
|
||||
@@ -59,8 +59,13 @@ QtObject {
|
||||
return;
|
||||
}
|
||||
const selectedEntry = entries[ClipboardService.selectedIndex];
|
||||
if (modal.activeTab === "saved") {
|
||||
if (selectedEntry.pinned) {
|
||||
modal.unpinEntry(selectedEntry);
|
||||
return;
|
||||
}
|
||||
const pinnedDuplicate = ClipboardService.getPinnedEntryByHash(selectedEntry.hash);
|
||||
if (pinnedDuplicate) {
|
||||
modal.unpinEntry(pinnedDuplicate);
|
||||
} else {
|
||||
modal.pinEntry(selectedEntry);
|
||||
}
|
||||
|
||||
@@ -727,6 +727,51 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
// Timeout progress bar: drains as the dismiss timer runs; inset by
|
||||
// the corner radius and frozen while hovered or during exit.
|
||||
Rectangle {
|
||||
id: timeoutBar
|
||||
|
||||
readonly property bool active: SettingsData.notificationShowTimeoutBar && notificationData && notificationData.timer && notificationData.timer.interval > 0
|
||||
property real progress: 1
|
||||
readonly property real surfaceRadius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
||||
|
||||
visible: active && progress > 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: surfaceRadius
|
||||
anchors.bottom: parent.bottom
|
||||
width: Math.max(0, parent.width - surfaceRadius * 2) * progress
|
||||
height: Math.max(2, Theme.snap(3, win.dpr))
|
||||
radius: height / 2
|
||||
z: 50
|
||||
opacity: 0.9
|
||||
color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Theme.error : Theme.primary
|
||||
|
||||
NumberAnimation {
|
||||
id: progressAnim
|
||||
target: timeoutBar
|
||||
property: "progress"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: (notificationData && notificationData.timer && notificationData.timer.interval > 0) ? notificationData.timer.interval : 5000
|
||||
running: timeoutBar.active && notificationData && notificationData.timer && notificationData.timer.running && !win.exiting
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
|
||||
// Reset to full on every (re)start, including an in-place
|
||||
// restart on a deduped notification (running stays true, so the
|
||||
// bound animation alone wouldn't re-fire).
|
||||
Connections {
|
||||
target: timeoutBar.active ? notificationData.timer : null
|
||||
function onRunningChanged() {
|
||||
if (notificationData && notificationData.timer && notificationData.timer.running && !win.exiting) {
|
||||
timeoutBar.progress = 1;
|
||||
progressAnim.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
@@ -877,10 +922,11 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
StyledText {
|
||||
text: notificationData ? (notificationData.summary || "") : ""
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.pixelSize: SettingsData.notificationSummaryFontSize || Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
@@ -896,7 +942,7 @@ PanelWindow {
|
||||
text: notificationData ? (notificationData.htmlBody || "") : ""
|
||||
textFormat: Text.StyledText
|
||||
color: Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.pixelSize: SettingsData.notificationBodyFontSize || Theme.fontSizeSmall
|
||||
width: parent.width
|
||||
elide: descriptionExpanded ? Text.ElideNone : Text.ElideRight
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
|
||||
@@ -722,7 +722,7 @@ Item {
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "system_tray"
|
||||
iconName: "handyman"
|
||||
title: I18n.tr("Tray Icon Fix")
|
||||
visible: DesktopService.isSystemd
|
||||
|
||||
|
||||
@@ -1754,6 +1754,9 @@ Item {
|
||||
text: I18n.tr("Y Axis")
|
||||
description: I18n.tr("Action performed when scrolling vertically on the bar")
|
||||
model: CompositorService.isNiri ? [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")] : [I18n.tr("None"), I18n.tr("Workspace")]
|
||||
buttonPadding: Theme.spacingS
|
||||
minButtonWidth: 44
|
||||
textSize: Theme.fontSizeSmall
|
||||
currentIndex: {
|
||||
switch (selectedBarConfig?.scrollYBehavior || "workspace") {
|
||||
case "none":
|
||||
@@ -1792,6 +1795,9 @@ Item {
|
||||
description: I18n.tr("Action performed when scrolling horizontally on the bar")
|
||||
visible: CompositorService.isNiri
|
||||
model: [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")]
|
||||
buttonPadding: Theme.spacingS
|
||||
minButtonWidth: 44
|
||||
textSize: Theme.fontSizeSmall
|
||||
currentIndex: {
|
||||
switch (selectedBarConfig?.scrollXBehavior || "column") {
|
||||
case "none":
|
||||
|
||||
@@ -205,6 +205,9 @@ Item {
|
||||
tags: ["frame", "border", "color", "theme", "primary", "surface", "default"]
|
||||
text: I18n.tr("Border Color")
|
||||
model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")]
|
||||
buttonPadding: Theme.spacingS
|
||||
minButtonWidth: 44
|
||||
textSize: Theme.fontSizeSmall
|
||||
currentIndex: {
|
||||
const fc = SettingsData.frameColor;
|
||||
if (!fc || fc === "default")
|
||||
|
||||
@@ -200,12 +200,40 @@ Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "notifications"
|
||||
title: I18n.tr("Notification Popups")
|
||||
settingKey: "notificationPopups"
|
||||
|
||||
// Font size selectors for summary and body
|
||||
SettingsDropdownRow {
|
||||
settingKey: "notificationSummaryFontSize"
|
||||
tags: ["notification", "font", "summary", "size"]
|
||||
text: I18n.tr("Summary Font Size")
|
||||
description: I18n.tr("Set the font size for notification summary text")
|
||||
options: [I18n.tr("Unset"), "10", "12", "14", "16", "18"]
|
||||
currentValue: (SettingsData.notificationSummaryFontSize || I18n.tr("Unset")).toString()
|
||||
onValueChanged: value => {
|
||||
SettingsData.set("notificationSummaryFontSize", Number(value === I18n.tr("Unset") ? 0 : value));
|
||||
SettingsData.sendTestNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
SettingsDropdownRow {
|
||||
settingKey: "notificationBodyFontSize"
|
||||
tags: ["notification", "font", "body", "size"]
|
||||
text: I18n.tr("Body Font Size")
|
||||
description: I18n.tr("Set the font size for notification body text (htmlBody)")
|
||||
options: [I18n.tr("Unset"), "10", "12", "14", "16", "18"]
|
||||
currentValue: (SettingsData.notificationBodyFontSize || I18n.tr("Unset")).toString()
|
||||
onValueChanged: value => {
|
||||
SettingsData.set("notificationBodyFontSize", Number(value === I18n.tr("Unset") ? 0 : value));
|
||||
SettingsData.sendTestNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
SettingsDropdownRow {
|
||||
settingKey: "notificationPopupPosition"
|
||||
tags: ["notification", "popup", "position", "screen", "location"]
|
||||
@@ -273,6 +301,15 @@ Item {
|
||||
onToggled: checked => SettingsData.set("notificationCompactMode", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "notificationShowTimeoutBar"
|
||||
tags: ["notification", "timeout", "progress", "bar", "timer", "countdown"]
|
||||
text: I18n.tr("Timeout Progress Bar")
|
||||
description: I18n.tr("Show a bar that drains as the popup's auto-dismiss timer runs")
|
||||
checked: SettingsData.notificationShowTimeoutBar
|
||||
onToggled: checked => SettingsData.set("notificationShowTimeoutBar", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "notificationDedupeEnabled"
|
||||
tags: ["notification", "duplicate", "dedupe", "stack", "coalesce", "repeat"]
|
||||
|
||||
@@ -2681,7 +2681,7 @@ Item {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "folder"
|
||||
name: "settings"
|
||||
size: 16
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
@@ -1271,6 +1271,7 @@ Item {
|
||||
tags: ["blur", "layer", "niri", "compositor"]
|
||||
title: I18n.tr("Blur Wallpaper Layer")
|
||||
settingKey: "blurWallpaper"
|
||||
iconName: "blur_on"
|
||||
visible: CompositorService.isNiri
|
||||
|
||||
SettingsToggleRow {
|
||||
|
||||
@@ -330,7 +330,7 @@ FloatingWindow {
|
||||
|
||||
delegate: Rectangle {
|
||||
width: widgetList.width
|
||||
height: 60
|
||||
height: Math.max(60, textColumn.implicitHeight + 24)
|
||||
radius: Theme.cornerRadius
|
||||
property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex
|
||||
color: isSelected ? Theme.withAlpha(Theme.primary, root.blurActive ? 0.22 : 0.16) : widgetArea.containsMouse ? Theme.withAlpha(Theme.primary, root.blurActive ? 0.14 : 0.08) : Theme.withAlpha(Theme.surfaceVariant, root.rowAlpha)
|
||||
@@ -351,9 +351,10 @@ FloatingWindow {
|
||||
}
|
||||
|
||||
Column {
|
||||
id: textColumn
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM * 3
|
||||
width: parent.width - Theme.iconSize * 2 - Theme.spacingM * 4 + 4
|
||||
|
||||
StyledText {
|
||||
text: modelData.text
|
||||
@@ -362,6 +363,7 @@ FloatingWindow {
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
StyledText {
|
||||
|
||||
@@ -90,7 +90,7 @@ Column {
|
||||
property real originalY: y
|
||||
|
||||
width: itemsList.width
|
||||
height: 70
|
||||
height: Math.max(70, textColumn.implicitHeight + 32)
|
||||
z: held ? 2 : 1
|
||||
|
||||
Rectangle {
|
||||
@@ -123,6 +123,7 @@ Column {
|
||||
}
|
||||
|
||||
Column {
|
||||
id: textColumn
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM * 3 + 40 + Theme.iconSize
|
||||
anchors.right: actionButtons.left
|
||||
@@ -137,6 +138,7 @@ Column {
|
||||
color: modelData.enabled ? Theme.surfaceText : Theme.outline
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
StyledText {
|
||||
|
||||
@@ -388,11 +388,15 @@ Singleton {
|
||||
return "text";
|
||||
}
|
||||
|
||||
function hashedPinnedEntry(entryHash) {
|
||||
function getPinnedEntryByHash(entryHash) {
|
||||
if (!entryHash) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
return pinnedEntries.some(pinnedEntry => pinnedEntry.hash === entryHash);
|
||||
return internalEntries.find(entry => entry.pinned && entry.hash === entryHash) || null;
|
||||
}
|
||||
|
||||
function hashedPinnedEntry(entryHash) {
|
||||
return getPinnedEntryByHash(entryHash) !== null;
|
||||
}
|
||||
|
||||
onClipboardAvailableChanged: {
|
||||
|
||||
@@ -966,4 +966,67 @@ Singleton {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
readonly property string _ipcIdPattern: "^[a-zA-Z0-9_\\-:]{1,64}$";
|
||||
|
||||
IpcHandler {
|
||||
target: "plugin-scan"
|
||||
|
||||
function scan(): string {
|
||||
root.scanPlugins();
|
||||
return `SCAN_TRIGGERED: ${Object.keys(root.availablePlugins).length} known before debounce`;
|
||||
}
|
||||
|
||||
function rescan(pluginId: string): string {
|
||||
if (!pluginId)
|
||||
return "ERROR: rescan requires a pluginId";
|
||||
if (!new RegExp(root._ipcIdPattern).test(pluginId))
|
||||
return `ERROR: invalid pluginId '${pluginId}' (allowed: [a-zA-Z0-9_\\-:]{1,64})`;
|
||||
if (!(pluginId in root.availablePlugins))
|
||||
return `ERROR: unknown pluginId '${pluginId}' (try 'list' first)`;
|
||||
root.forceRescanPlugin(pluginId);
|
||||
return `RESCAN_TRIGGERED: ${pluginId}`;
|
||||
}
|
||||
|
||||
function reload(pluginId: string): string {
|
||||
if (!pluginId)
|
||||
return "ERROR: reload requires a pluginId";
|
||||
if (!new RegExp(root._ipcIdPattern).test(pluginId))
|
||||
return `ERROR: invalid pluginId '${pluginId}' (allowed: [a-zA-Z0-9_\\-:]{1,64})`;
|
||||
if (!(pluginId in root.availablePlugins))
|
||||
return `ERROR: unknown pluginId '${pluginId}'`;
|
||||
root.reloadPlugin(pluginId);
|
||||
return `RELOAD_TRIGGERED: ${pluginId}`;
|
||||
}
|
||||
|
||||
function list(): string {
|
||||
const ids = Object.keys(root.availablePlugins);
|
||||
const cap = 256;
|
||||
const n = Math.min(ids.length, cap);
|
||||
const lines = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const id = ids[i];
|
||||
if (!new RegExp(root._ipcIdPattern).test(id))
|
||||
continue;
|
||||
const p = root.availablePlugins[id];
|
||||
const safeName = String(p.name || "").replace(/[\t\n\r]/g, " ");
|
||||
lines.push(`${id}\t${p.loaded ? "loaded" : "unloaded"}\t${p.type || "unknown"}\t${safeName}`);
|
||||
}
|
||||
const header = `# count=${ids.length} returned=${n}${ids.length > n ? " (truncated, see cap)" : ""}`;
|
||||
return header + "\n" + lines.join("\n");
|
||||
}
|
||||
|
||||
function status(pluginId: string): string {
|
||||
if (!pluginId)
|
||||
return "ERROR: status requires a pluginId";
|
||||
if (!new RegExp(root._ipcIdPattern).test(pluginId))
|
||||
return `ERROR: invalid pluginId '${pluginId}'`;
|
||||
const plugin = root.availablePlugins[pluginId];
|
||||
if (!plugin)
|
||||
return `ERROR: unknown pluginId '${pluginId}'`;
|
||||
const err = root.pluginLoadErrors[pluginId] || "";
|
||||
const safeErr = String(err).replace(/[\t\n\r]/g, " ");
|
||||
return `${plugin.loaded ? "loaded" : "unloaded"}\t${plugin.type || ""}\t${safeErr}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user