1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-13 09:12:08 -04:00

clipboard: Fix pinned entry logic

- Add keyboard nav to pinned entries
- Fix wrong copied selection upon Enter
This commit is contained in:
purian23
2026-02-09 20:53:48 -05:00
parent a168b12bb2
commit 0922e3e459
7 changed files with 158 additions and 15 deletions

View File

@@ -146,9 +146,16 @@ func handleCopyEntry(conn net.Conn, req models.Request, m *Manager) {
return return
} }
if err := m.TouchEntry(uint64(id)); err != nil { if entry.Pinned {
models.RespondError(conn, req.ID, err.Error()) if err := m.CreateHistoryEntryFromPinned(entry); err != nil {
return models.RespondError(conn, req.ID, err.Error())
return
}
} else {
if err := m.TouchEntry(uint64(id)); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
} }
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"})

View File

@@ -388,6 +388,10 @@ func (m *Manager) deduplicateInTx(b *bolt.Bucket, hash uint64) error {
if extractHash(v) != hash { if extractHash(v) != hash {
continue continue
} }
entry, err := decodeEntry(v)
if err == nil && entry.Pinned {
continue
}
if err := b.Delete(k); err != nil { if err := b.Delete(k); err != nil {
return err return err
} }
@@ -842,6 +846,62 @@ func (m *Manager) TouchEntry(id uint64) error {
return nil return nil
} }
func (m *Manager) CreateHistoryEntryFromPinned(pinnedEntry *Entry) error {
if m.db == nil {
return fmt.Errorf("database not available")
}
// Create a new unpinned entry with the same data
newEntry := Entry{
Data: pinnedEntry.Data,
MimeType: pinnedEntry.MimeType,
Size: pinnedEntry.Size,
Timestamp: time.Now(),
IsImage: pinnedEntry.IsImage,
Preview: pinnedEntry.Preview,
Pinned: false,
}
if err := m.storeEntryWithoutDedup(newEntry); err != nil {
return err
}
m.updateState()
m.notifySubscribers()
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
@@ -1419,6 +1479,37 @@ func (m *Manager) PinEntry(id uint64) error {
return fmt.Errorf("database not available") return fmt.Errorf("database not available")
} }
entryToPin, err := m.GetEntry(id)
if err != nil {
return err
}
var hashExists bool
if err := m.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard"))
if b == nil {
return nil
}
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
entry, err := decodeEntry(v)
if err != nil || !entry.Pinned {
continue
}
if entry.Hash == entryToPin.Hash {
hashExists = true
return nil
}
}
return nil
}); err != nil {
return err
}
if hashExists {
return nil
}
// Check pinned count // Check pinned count
cfg := m.getConfig() cfg := m.getConfig()
pinnedCount := 0 pinnedCount := 0
@@ -1443,7 +1534,7 @@ func (m *Manager) PinEntry(id uint64) error {
return fmt.Errorf("maximum pinned entries reached (%d)", cfg.MaxPinned) return fmt.Errorf("maximum pinned entries reached (%d)", cfg.MaxPinned)
} }
err := m.db.Update(func(tx *bolt.Tx) error { err = m.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard")) b := tx.Bucket([]byte("clipboard"))
v := b.Get(itob(id)) v := b.Get(itob(id))
if v == nil { if v == nil {

View File

@@ -164,6 +164,7 @@ Item {
} }
visible: modal.activeTab === "saved" visible: modal.activeTab === "saved"
currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
spacing: Theme.spacingXS spacing: Theme.spacingXS
interactive: true interactive: true
flickDeceleration: 1500 flickDeceleration: 1500
@@ -173,6 +174,26 @@ Item {
pressDelay: 0 pressDelay: 0
flickableDirection: Flickable.VerticalFlick flickableDirection: Flickable.VerticalFlick
function ensureVisible(index) {
if (index < 0 || index >= count) {
return;
}
const itemHeight = ClipboardConstants.itemHeight + spacing;
const itemY = index * itemHeight;
const itemBottom = itemY + itemHeight;
if (itemY < contentY) {
contentY = itemY;
} else if (itemBottom > contentY + height) {
contentY = itemBottom - height;
}
}
onCurrentIndexChanged: {
if (clipboardContent.modal?.keyboardNavigationActive && currentIndex >= 0) {
ensureVisible(currentIndex);
}
}
StyledText { StyledText {
text: I18n.tr("No saved clipboard entries") text: I18n.tr("No saved clipboard entries")
anchors.centerIn: parent anchors.centerIn: parent
@@ -190,7 +211,7 @@ Item {
entry: modelData entry: modelData
entryIndex: index + 1 entryIndex: index + 1
itemIndex: index itemIndex: index
isSelected: false isSelected: clipboardContent.modal?.keyboardNavigationActive && index === clipboardContent.modal.selectedIndex
modal: clipboardContent.modal modal: clipboardContent.modal
listView: savedListView listView: savedListView
onCopyRequested: clipboardContent.modal.copyEntry(modelData) onCopyRequested: clipboardContent.modal.copyEntry(modelData)

View File

@@ -19,6 +19,7 @@ Rectangle {
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 && modal ? modal.hashedPinnedEntry(entry.hash) : false
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
@@ -111,8 +112,8 @@ Rectangle {
DankActionButton { DankActionButton {
iconName: "push_pin" iconName: "push_pin"
iconSize: Theme.iconSize - 6 iconSize: Theme.iconSize - 6
iconColor: entry.pinned ? Theme.primary : Theme.surfaceText iconColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primary : Theme.surfaceText
backgroundColor: entry.pinned ? Theme.primarySelected : "transparent" backgroundColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primarySelected : "transparent"
onClicked: entry.pinned ? unpinRequested() : pinRequested() onClicked: entry.pinned ? unpinRequested() : pinRequested()
} }

View File

@@ -18,6 +18,10 @@ DankModal {
} }
property string activeTab: "recents" property string activeTab: "recents"
onActiveTabChanged: {
ClipboardService.selectedIndex = 0;
ClipboardService.keyboardNavigationActive = false;
}
property bool showKeyboardHints: false property bool showKeyboardHints: false
property Component clipboardContent property Component clipboardContent
property int activeImageLoads: 0 property int activeImageLoads: 0
@@ -121,6 +125,10 @@ DankModal {
return ClipboardService.getEntryType(entry); return ClipboardService.getEntryType(entry);
} }
function hashedPinnedEntry(entryHash) {
return ClipboardService.hashedPinnedEntry(entryHash);
}
visible: false visible: false
modalWidth: ClipboardConstants.modalWidth modalWidth: ClipboardConstants.modalWidth
modalHeight: ClipboardConstants.modalHeight modalHeight: ClipboardConstants.modalHeight

View File

@@ -13,15 +13,17 @@ QtObject {
} }
function selectNext() { function selectNext() {
if (!ClipboardService.clipboardEntries || ClipboardService.clipboardEntries.length === 0) { const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
if (!entries || entries.length === 0) {
return; return;
} }
ClipboardService.keyboardNavigationActive = true; ClipboardService.keyboardNavigationActive = true;
ClipboardService.selectedIndex = Math.min(ClipboardService.selectedIndex + 1, ClipboardService.clipboardEntries.length - 1); ClipboardService.selectedIndex = Math.min(ClipboardService.selectedIndex + 1, entries.length - 1);
} }
function selectPrevious() { function selectPrevious() {
if (!ClipboardService.clipboardEntries || ClipboardService.clipboardEntries.length === 0) { const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
if (!entries || entries.length === 0) {
return; return;
} }
ClipboardService.keyboardNavigationActive = true; ClipboardService.keyboardNavigationActive = true;
@@ -29,19 +31,25 @@ QtObject {
} }
function copySelected() { function copySelected() {
if (!ClipboardService.clipboardEntries || ClipboardService.clipboardEntries.length === 0 || ClipboardService.selectedIndex < 0 || ClipboardService.selectedIndex >= ClipboardService.clipboardEntries.length) { const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
if (!entries || entries.length === 0 || ClipboardService.selectedIndex < 0 || ClipboardService.selectedIndex >= entries.length) {
return; return;
} }
const selectedEntry = ClipboardService.clipboardEntries[ClipboardService.selectedIndex]; const selectedEntry = entries[ClipboardService.selectedIndex];
modal.copyEntry(selectedEntry); modal.copyEntry(selectedEntry);
} }
function deleteSelected() { function deleteSelected() {
if (!ClipboardService.clipboardEntries || ClipboardService.clipboardEntries.length === 0 || ClipboardService.selectedIndex < 0 || ClipboardService.selectedIndex >= ClipboardService.clipboardEntries.length) { const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
if (!entries || entries.length === 0 || ClipboardService.selectedIndex < 0 || ClipboardService.selectedIndex >= entries.length) {
return; return;
} }
const selectedEntry = ClipboardService.clipboardEntries[ClipboardService.selectedIndex]; const selectedEntry = entries[ClipboardService.selectedIndex];
modal.deleteEntry(selectedEntry); if (modal.activeTab === "saved") {
modal.deletePinnedEntry(selectedEntry);
} else {
modal.deleteEntry(selectedEntry);
}
} }
function handleKey(event) { function handleKey(event) {

View File

@@ -248,6 +248,13 @@ Singleton {
return "text"; return "text";
} }
function hashedPinnedEntry(entryHash) {
if (!entryHash) {
return false;
}
return pinnedEntries.some(pinnedEntry => pinnedEntry.hash === entryHash);
}
Connections { Connections {
target: DMSService target: DMSService
enabled: root.refCount > 0 enabled: root.refCount > 0