mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-17 08:35:21 -04:00
Compare commits
5 Commits
cd672c341f
...
08fd6e26d8
| Author | SHA1 | Date | |
|---|---|---|---|
| 08fd6e26d8 | |||
| 29e8470f2e | |||
| 573785d4ce | |||
| 5483303714 | |||
| 5a5cc4f4e9 |
@@ -115,3 +115,5 @@ core.*
|
|||||||
.direnv/
|
.direnv/
|
||||||
quickshell/dms-plugins
|
quickshell/dms-plugins
|
||||||
__pycache__
|
__pycache__
|
||||||
|
|
||||||
|
.vscode/
|
||||||
|
|||||||
@@ -935,7 +935,7 @@ func (m *Manager) CreateHistoryEntryFromPinned(pinnedEntry *Entry) error {
|
|||||||
Pinned: false,
|
Pinned: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.storeEntryWithoutDedup(newEntry); err != nil {
|
if err := m.storeEntry(newEntry); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -945,36 +945,6 @@ func (m *Manager) CreateHistoryEntryFromPinned(pinnedEntry *Entry) error {
|
|||||||
return nil
|
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() {
|
func (m *Manager) ClearHistory() {
|
||||||
if m.db == nil {
|
if m.db == nil {
|
||||||
return
|
return
|
||||||
@@ -1653,6 +1623,37 @@ func (m *Manager) UnpinEntry(id uint64) error {
|
|||||||
return err
|
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
|
entry.Pinned = false
|
||||||
encoded, err := encodeEntry(entry)
|
encoded, err := encodeEntry(entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
bolt "go.etcd.io/bbolt"
|
||||||
|
|
||||||
mocks_wlcontext "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlcontext"
|
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)
|
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) {
|
func TestManager_ConcurrentSubscriberAccess(t *testing.T) {
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
subscribers: make(map[string]chan State),
|
subscribers: make(map[string]chan State),
|
||||||
|
|||||||
@@ -515,6 +515,8 @@ Singleton {
|
|||||||
property bool notepadUseMonospace: true
|
property bool notepadUseMonospace: true
|
||||||
property string notepadFontFamily: ""
|
property string notepadFontFamily: ""
|
||||||
property real notepadFontSize: 14
|
property real notepadFontSize: 14
|
||||||
|
property real notificationSummaryFontSize: Spec.SPEC.notificationSummaryFontSize.def
|
||||||
|
property real notificationBodyFontSize: Spec.SPEC.notificationBodyFontSize.def
|
||||||
property bool notepadShowLineNumbers: false
|
property bool notepadShowLineNumbers: false
|
||||||
property real notepadTransparencyOverride: -1
|
property real notepadTransparencyOverride: -1
|
||||||
property real notepadLastCustomTransparency: 0.7
|
property real notepadLastCustomTransparency: 0.7
|
||||||
@@ -695,6 +697,7 @@ Singleton {
|
|||||||
property int notificationTimeoutNormal: 5000
|
property int notificationTimeoutNormal: 5000
|
||||||
property int notificationTimeoutCritical: 0
|
property int notificationTimeoutCritical: 0
|
||||||
property bool notificationCompactMode: false
|
property bool notificationCompactMode: false
|
||||||
|
property bool notificationShowTimeoutBar: false
|
||||||
property bool notificationDedupeEnabled: true
|
property bool notificationDedupeEnabled: true
|
||||||
property int notificationPopupPosition: SettingsData.Position.Top
|
property int notificationPopupPosition: SettingsData.Position.Top
|
||||||
property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short
|
property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short
|
||||||
|
|||||||
@@ -260,6 +260,8 @@ var SPEC = {
|
|||||||
notepadUseMonospace: { def: true },
|
notepadUseMonospace: { def: true },
|
||||||
notepadFontFamily: { def: "" },
|
notepadFontFamily: { def: "" },
|
||||||
notepadFontSize: { def: 14 },
|
notepadFontSize: { def: 14 },
|
||||||
|
notificationSummaryFontSize: { def: 0 },
|
||||||
|
notificationBodyFontSize: { def: 0 },
|
||||||
notepadShowLineNumbers: { def: false },
|
notepadShowLineNumbers: { def: false },
|
||||||
notepadTransparencyOverride: { def: -1 },
|
notepadTransparencyOverride: { def: -1 },
|
||||||
notepadLastCustomTransparency: { def: 0.7 },
|
notepadLastCustomTransparency: { def: 0.7 },
|
||||||
@@ -406,6 +408,7 @@ var SPEC = {
|
|||||||
notificationTimeoutNormal: { def: 5000 },
|
notificationTimeoutNormal: { def: 5000 },
|
||||||
notificationTimeoutCritical: { def: 0 },
|
notificationTimeoutCritical: { def: 0 },
|
||||||
notificationCompactMode: { def: false },
|
notificationCompactMode: { def: false },
|
||||||
|
notificationShowTimeoutBar: { def: false },
|
||||||
notificationDedupeEnabled: { def: true },
|
notificationDedupeEnabled: { def: true },
|
||||||
notificationPopupPosition: { def: 0 },
|
notificationPopupPosition: { def: 0 },
|
||||||
notificationAnimationSpeed: { def: 1 },
|
notificationAnimationSpeed: { def: 1 },
|
||||||
|
|||||||
@@ -149,8 +149,8 @@ Item {
|
|||||||
listView: clipboardListView
|
listView: clipboardListView
|
||||||
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
||||||
onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
|
onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
|
||||||
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
onPinRequested: targetEntry => clipboardContent.modal.pinEntry(targetEntry)
|
||||||
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
onUnpinRequested: targetEntry => clipboardContent.modal.unpinEntry(targetEntry)
|
||||||
onEditRequested: clipboardContent.modal.editEntry(modelData)
|
onEditRequested: clipboardContent.modal.editEntry(modelData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,8 +223,8 @@ Item {
|
|||||||
listView: savedListView
|
listView: savedListView
|
||||||
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
||||||
onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
|
onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
|
||||||
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
onPinRequested: targetEntry => clipboardContent.modal.pinEntry(targetEntry)
|
||||||
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
onUnpinRequested: targetEntry => clipboardContent.modal.unpinEntry(targetEntry)
|
||||||
onEditRequested: clipboardContent.modal.editEntry(modelData)
|
onEditRequested: clipboardContent.modal.editEntry(modelData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,14 @@ Rectangle {
|
|||||||
|
|
||||||
signal copyRequested
|
signal copyRequested
|
||||||
signal deleteRequested
|
signal deleteRequested
|
||||||
signal pinRequested
|
signal pinRequested(var targetEntry)
|
||||||
signal unpinRequested
|
signal unpinRequested(var targetEntry)
|
||||||
signal editRequested
|
signal editRequested
|
||||||
|
|
||||||
readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
|
readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
|
||||||
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
|
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
|
radius: Theme.cornerRadius
|
||||||
color: {
|
color: {
|
||||||
@@ -66,9 +67,19 @@ Rectangle {
|
|||||||
DankActionButton {
|
DankActionButton {
|
||||||
iconName: "push_pin"
|
iconName: "push_pin"
|
||||||
iconSize: Theme.iconSize - 6
|
iconSize: Theme.iconSize - 6
|
||||||
iconColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primary : Theme.surfaceText
|
iconColor: effectivePinned ? Theme.primary : Theme.surfaceText
|
||||||
backgroundColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primarySelected : "transparent"
|
backgroundColor: effectivePinned ? Theme.primarySelected : "transparent"
|
||||||
onClicked: entry.pinned ? unpinRequested() : pinRequested()
|
onClicked: {
|
||||||
|
if (entry.pinned) {
|
||||||
|
unpinRequested(entry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pinnedDuplicateEntry) {
|
||||||
|
unpinRequested(pinnedDuplicateEntry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pinRequested(entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
|
|||||||
@@ -59,8 +59,13 @@ QtObject {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const selectedEntry = entries[ClipboardService.selectedIndex];
|
const selectedEntry = entries[ClipboardService.selectedIndex];
|
||||||
if (modal.activeTab === "saved") {
|
if (selectedEntry.pinned) {
|
||||||
modal.unpinEntry(selectedEntry);
|
modal.unpinEntry(selectedEntry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pinnedDuplicate = ClipboardService.getPinnedEntryByHash(selectedEntry.hash);
|
||||||
|
if (pinnedDuplicate) {
|
||||||
|
modal.unpinEntry(pinnedDuplicate);
|
||||||
} else {
|
} else {
|
||||||
modal.pinEntry(selectedEntry);
|
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.enabled: I18n.isRtl
|
||||||
LayoutMirroring.childrenInherit: true
|
LayoutMirroring.childrenInherit: true
|
||||||
|
|
||||||
@@ -877,10 +922,11 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: notificationData ? (notificationData.summary || "") : ""
|
text: notificationData ? (notificationData.summary || "") : ""
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: SettingsData.notificationSummaryFontSize || Theme.fontSizeMedium
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
width: parent.width
|
width: parent.width
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
@@ -896,7 +942,7 @@ PanelWindow {
|
|||||||
text: notificationData ? (notificationData.htmlBody || "") : ""
|
text: notificationData ? (notificationData.htmlBody || "") : ""
|
||||||
textFormat: Text.StyledText
|
textFormat: Text.StyledText
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: SettingsData.notificationBodyFontSize || Theme.fontSizeSmall
|
||||||
width: parent.width
|
width: parent.width
|
||||||
elide: descriptionExpanded ? Text.ElideNone : Text.ElideRight
|
elide: descriptionExpanded ? Text.ElideNone : Text.ElideRight
|
||||||
horizontalAlignment: Text.AlignLeft
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
|||||||
@@ -722,7 +722,7 @@ Item {
|
|||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
iconName: "system_tray"
|
iconName: "handyman"
|
||||||
title: I18n.tr("Tray Icon Fix")
|
title: I18n.tr("Tray Icon Fix")
|
||||||
visible: DesktopService.isSystemd
|
visible: DesktopService.isSystemd
|
||||||
|
|
||||||
|
|||||||
@@ -1754,6 +1754,9 @@ Item {
|
|||||||
text: I18n.tr("Y Axis")
|
text: I18n.tr("Y Axis")
|
||||||
description: I18n.tr("Action performed when scrolling vertically on the bar")
|
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")]
|
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: {
|
currentIndex: {
|
||||||
switch (selectedBarConfig?.scrollYBehavior || "workspace") {
|
switch (selectedBarConfig?.scrollYBehavior || "workspace") {
|
||||||
case "none":
|
case "none":
|
||||||
@@ -1792,6 +1795,9 @@ Item {
|
|||||||
description: I18n.tr("Action performed when scrolling horizontally on the bar")
|
description: I18n.tr("Action performed when scrolling horizontally on the bar")
|
||||||
visible: CompositorService.isNiri
|
visible: CompositorService.isNiri
|
||||||
model: [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")]
|
model: [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")]
|
||||||
|
buttonPadding: Theme.spacingS
|
||||||
|
minButtonWidth: 44
|
||||||
|
textSize: Theme.fontSizeSmall
|
||||||
currentIndex: {
|
currentIndex: {
|
||||||
switch (selectedBarConfig?.scrollXBehavior || "column") {
|
switch (selectedBarConfig?.scrollXBehavior || "column") {
|
||||||
case "none":
|
case "none":
|
||||||
|
|||||||
@@ -205,6 +205,9 @@ Item {
|
|||||||
tags: ["frame", "border", "color", "theme", "primary", "surface", "default"]
|
tags: ["frame", "border", "color", "theme", "primary", "surface", "default"]
|
||||||
text: I18n.tr("Border Color")
|
text: I18n.tr("Border Color")
|
||||||
model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")]
|
model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")]
|
||||||
|
buttonPadding: Theme.spacingS
|
||||||
|
minButtonWidth: 44
|
||||||
|
textSize: Theme.fontSizeSmall
|
||||||
currentIndex: {
|
currentIndex: {
|
||||||
const fc = SettingsData.frameColor;
|
const fc = SettingsData.frameColor;
|
||||||
if (!fc || fc === "default")
|
if (!fc || fc === "default")
|
||||||
|
|||||||
@@ -200,12 +200,40 @@ Item {
|
|||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
spacing: Theme.spacingXL
|
spacing: Theme.spacingXL
|
||||||
|
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
iconName: "notifications"
|
iconName: "notifications"
|
||||||
title: I18n.tr("Notification Popups")
|
title: I18n.tr("Notification Popups")
|
||||||
settingKey: "notificationPopups"
|
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 {
|
SettingsDropdownRow {
|
||||||
settingKey: "notificationPopupPosition"
|
settingKey: "notificationPopupPosition"
|
||||||
tags: ["notification", "popup", "position", "screen", "location"]
|
tags: ["notification", "popup", "position", "screen", "location"]
|
||||||
@@ -273,6 +301,15 @@ Item {
|
|||||||
onToggled: checked => SettingsData.set("notificationCompactMode", checked)
|
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 {
|
SettingsToggleRow {
|
||||||
settingKey: "notificationDedupeEnabled"
|
settingKey: "notificationDedupeEnabled"
|
||||||
tags: ["notification", "duplicate", "dedupe", "stack", "coalesce", "repeat"]
|
tags: ["notification", "duplicate", "dedupe", "stack", "coalesce", "repeat"]
|
||||||
|
|||||||
@@ -2681,7 +2681,7 @@ Item {
|
|||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: "folder"
|
name: "settings"
|
||||||
size: 16
|
size: 16
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|||||||
@@ -1271,6 +1271,7 @@ Item {
|
|||||||
tags: ["blur", "layer", "niri", "compositor"]
|
tags: ["blur", "layer", "niri", "compositor"]
|
||||||
title: I18n.tr("Blur Wallpaper Layer")
|
title: I18n.tr("Blur Wallpaper Layer")
|
||||||
settingKey: "blurWallpaper"
|
settingKey: "blurWallpaper"
|
||||||
|
iconName: "blur_on"
|
||||||
visible: CompositorService.isNiri
|
visible: CompositorService.isNiri
|
||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ FloatingWindow {
|
|||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
width: widgetList.width
|
width: widgetList.width
|
||||||
height: 60
|
height: Math.max(60, textColumn.implicitHeight + 24)
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex
|
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)
|
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 {
|
Column {
|
||||||
|
id: textColumn
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: 2
|
spacing: 2
|
||||||
width: parent.width - Theme.iconSize - Theme.spacingM * 3
|
width: parent.width - Theme.iconSize * 2 - Theme.spacingM * 4 + 4
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: modelData.text
|
text: modelData.text
|
||||||
@@ -362,6 +363,7 @@ FloatingWindow {
|
|||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ Column {
|
|||||||
property real originalY: y
|
property real originalY: y
|
||||||
|
|
||||||
width: itemsList.width
|
width: itemsList.width
|
||||||
height: 70
|
height: Math.max(70, textColumn.implicitHeight + 32)
|
||||||
z: held ? 2 : 1
|
z: held ? 2 : 1
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -123,6 +123,7 @@ Column {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
id: textColumn
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: Theme.spacingM * 3 + 40 + Theme.iconSize
|
anchors.leftMargin: Theme.spacingM * 3 + 40 + Theme.iconSize
|
||||||
anchors.right: actionButtons.left
|
anchors.right: actionButtons.left
|
||||||
@@ -137,6 +138,7 @@ Column {
|
|||||||
color: modelData.enabled ? Theme.surfaceText : Theme.outline
|
color: modelData.enabled ? Theme.surfaceText : Theme.outline
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
|
|||||||
@@ -388,11 +388,15 @@ Singleton {
|
|||||||
return "text";
|
return "text";
|
||||||
}
|
}
|
||||||
|
|
||||||
function hashedPinnedEntry(entryHash) {
|
function getPinnedEntryByHash(entryHash) {
|
||||||
if (!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: {
|
onClipboardAvailableChanged: {
|
||||||
|
|||||||
@@ -966,4 +966,67 @@ Singleton {
|
|||||||
}
|
}
|
||||||
return result;
|
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