Skip to content

Commit 762f074

Browse files
Merge branch 'develop'
2 parents 61c3380 + 0206ad7 commit 762f074

9 files changed

+105
-120
lines changed

src/clipboard.ts

+35-46
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import clipboardEvent from "clipboard-event";
22
import * as electron from "electron";
33
import { storage } from "./storage";
4-
import type { ClipItem, ClipItemMeta, ClipItemType } from "./types/types";
5-
import { hash } from "./util/hash";
4+
import type { ClipItem, ClipItemImage, ClipItemType } from "./types/types";
5+
import { getNextClipItemId } from "./util/clipItemId";
6+
import { createHash } from "./util/createHash";
67
import { toMarkdownImageLink } from "./util/transformations";
78

89
// Clipboards containing any of these formats should not be saved
@@ -25,59 +26,48 @@ function read(): ClipItem | null {
2526
return null;
2627
}
2728

29+
const created = Date.now();
30+
const id = getNextClipItemId(created);
2831
const text = electron.clipboard.readText() || undefined;
2932
const rtf = electron.clipboard.readRTF() || undefined;
3033
const html = electron.clipboard.readHTML() || undefined;
31-
const bookmark = (() => {
32-
const bookmark = electron.clipboard.readBookmark();
33-
return bookmark.title || bookmark.url ? bookmark : undefined;
34-
})();
35-
const image = (() => {
36-
const nativeImage = electron.clipboard.readImage();
37-
return nativeImage.isEmpty() ? undefined : nativeImage.toDataURL();
38-
})();
39-
const type: ClipItemType = image ? "image" : "text";
40-
let id: string | undefined;
41-
let meta: ClipItemMeta | undefined;
42-
43-
if (image) {
44-
id = `image_${hash(image)}`;
45-
46-
if (html) {
47-
meta = {
48-
src: /<img.*?src=(?:"(.+?)"|'(.+?)').*?>/g.exec(html)?.[1],
49-
alt: /<img.*?alt=(?:"(.+?)"|'(.+?)').*?>/g.exec(html)?.[1],
50-
};
51-
}
52-
} else {
53-
const value = text ?? rtf ?? html;
54-
if (value) {
55-
id = `text_${hash(value)}`;
56-
}
57-
}
34+
const image = redImage(html);
35+
const bookmark = readBookmark();
36+
const type: ClipItemType = image != null ? "image" : "text";
37+
const hash = createHash(image?.data ?? text ?? rtf ?? html ?? "");
5838

59-
const item = {
39+
return {
40+
id,
41+
created,
42+
hash,
6043
type,
44+
name: undefined,
45+
list: undefined,
6146
text,
6247
rtf,
6348
html,
6449
bookmark,
65-
meta,
6650
image,
6751
};
52+
}
6853

69-
if (id == null) {
70-
console.error("Missing id", formats, item);
71-
return null;
54+
function redImage(html: string | undefined): ClipItemImage | undefined {
55+
const nativeImage = electron.clipboard.readImage();
56+
if (nativeImage.isEmpty()) {
57+
return undefined;
7258
}
73-
7459
return {
75-
id,
76-
created: Date.now(),
77-
...item,
60+
src: html != null ? /<img.*?src=(?:"(.+?)"|'(.+?)').*?>/g.exec(html)?.[1] : undefined,
61+
alt: html != null ? /<img.*?alt=(?:"(.+?)"|'(.+?)').*?>/g.exec(html)?.[1] : undefined,
62+
data: nativeImage.toDataURL(),
7863
};
7964
}
8065

66+
function readBookmark(): electron.ReadBookmark | undefined {
67+
const bookmark = electron.clipboard.readBookmark();
68+
return bookmark.title || bookmark.url ? bookmark : undefined;
69+
}
70+
8171
function write(items: ClipItem[]) {
8272
if (items.length === 1) {
8373
writeItem(items[0]);
@@ -91,12 +81,12 @@ function writeItems(items: ClipItem[]) {
9181
for (const item of items) {
9282
switch (item.type) {
9383
case "text":
94-
texts.push(item.text ?? "[TEXT]");
84+
texts.push(item.text ?? item.rtf ?? item.html ?? "");
9585
break;
9686
case "image": {
97-
const name = item.name ?? item.meta?.alt ?? "[IMAGE]";
98-
if (item.meta?.src) {
99-
texts.push(toMarkdownImageLink(name, item.meta.src));
87+
const name = item.name ?? item.image?.alt ?? "IMAGE";
88+
if (item.image?.src) {
89+
texts.push(toMarkdownImageLink(name, item.image.src));
10090
} else {
10191
texts.push(name);
10292
}
@@ -114,11 +104,11 @@ function writeItem(item: ClipItem) {
114104
rtf,
115105
html,
116106
bookmark: bookmark?.title,
117-
image: image != null ? electron.nativeImage.createFromDataURL(image) : undefined,
107+
image: image != null ? electron.nativeImage.createFromDataURL(image.data) : undefined,
118108
});
119109
}
120110

121-
function onChange(callback: (item: ClipItem) => void) {
111+
function onChange(listener: (item: ClipItem) => void) {
122112
clipboardEvent.startListening();
123113

124114
clipboardEvent.on("change", () => {
@@ -129,13 +119,12 @@ function onChange(callback: (item: ClipItem) => void) {
129119
const item = read();
130120

131121
if (item != null) {
132-
callback(item);
122+
listener(item);
133123
}
134124
});
135125
}
136126

137127
export const clipboard = {
138-
read,
139128
write,
140129
onChange,
141130
};

src/clipboardList.ts

+11-25
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,47 @@
11
import { clipboard } from "./clipboard";
22
import { storage } from "./storage";
3-
import type { Target } from "./types/targets";
43
import { AllList, StarredList, UnstarredList, type ClipItem } from "./types/types";
5-
import { processTargets } from "./util/processTargets";
64

75
let t1 = 0;
86

9-
export function onChange(callback: () => void) {
7+
export function onChange(listener: () => void) {
108
clipboard.onChange((item) => {
119
const t2 = Date.now();
12-
addNewItem(item);
1310

14-
callback();
1511
const items = storage.getClipboardItems();
1612

17-
if (item.id !== items[0]?.id) {
13+
// Don't add the same item multiple times in a sequence
14+
if (item.hash !== items[0]?.hash) {
1815
// FIXME: Try to detect quick changes that are then reverted.
1916
// Remove this once we have proper transient formats from Talon side.
20-
if (item.id === items[1]?.id && t2 - t1 < 300) {
17+
if (item.hash === items[1]?.hash && t2 - t1 < 300) {
2118
storage.removeItems([items[0]]);
2219
} else {
2320
addNewItem(item);
2421
}
2522

26-
callback();
23+
listener();
2724
}
2825

2926
t1 = t2;
3027
});
3128
}
3229

33-
export function removeTargets(targets: Target[]) {
34-
const targetItems = processTargets(targets);
35-
storage.removeItems(targetItems);
36-
}
37-
3830
function addNewItem(item: ClipItem) {
39-
const items = storage.getClipboardItems();
40-
const existing = items.find((i) => i.id === item.id);
4131
const { autoStar, activeList } = storage.getConfig();
4232

4333
if (autoStar) {
44-
switch (activeList) {
45-
case AllList:
46-
case UnstarredList:
34+
switch (activeList.id) {
35+
case AllList.id:
4736
item.list = StarredList.id;
4837
break;
38+
case UnstarredList.id:
39+
// Do nothing
40+
break;
4941
default:
5042
item.list = activeList.id;
5143
}
5244
}
5345

54-
if (existing != null) {
55-
item.name = existing.name;
56-
item.list = item.list ?? existing.list;
57-
storage.addExistingItem(item);
58-
} else {
59-
storage.addNewItem(item);
60-
}
46+
storage.addNewItem(item);
6147
}

src/commands/removeItems.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import * as clipboardList from "../clipboardList";
1+
import { storage } from "../storage";
22
import type { RemoveItemsCommand } from "../types/command";
3+
import { processTargets } from "../util/processTargets";
34
import { updateRenderer } from "../util/updateRenderer";
45

56
export function removeItems(command: RemoveItemsCommand) {
6-
clipboardList.removeTargets(command.targets);
7+
const targetItems = processTargets(command.targets);
8+
9+
storage.removeItems(targetItems);
710

811
updateRenderer();
912
}

src/renderer/ClipboardList.tsx

+14-14
Original file line numberDiff line numberDiff line change
@@ -205,25 +205,25 @@ export function ClipboardList({ items }: Props): JSX.Element {
205205
function renderItemContent(item: ClipItem): JSX.Element {
206206
if (item.image != null) {
207207
return (
208-
<div className="clip-content-image" title={item.meta?.alt}>
209-
<img src={item.image} alt={item.meta?.alt} />
208+
<div className="clip-content-image" title={item.image.alt}>
209+
<img src={item.image.data} alt={item.image.alt} />
210210
</div>
211211
);
212212
}
213-
const text = item.text ?? item.rtf;
214-
if (text != null) {
215-
if (text.includes("\n")) {
216-
return (
217-
<pre className="clip-content-text" title={text}>
218-
{text}
219-
</pre>
220-
);
221-
}
213+
214+
const text = item.text ?? item.rtf ?? item.html ?? "";
215+
216+
if (text.includes("\n")) {
222217
return (
223-
<div className="clip-content-text" title={text}>
218+
<pre className="clip-content-text" title={text}>
224219
{text}
225-
</div>
220+
</pre>
226221
);
227222
}
228-
return <div>[FAILED TO RENDER]</div>;
223+
224+
return (
225+
<div className="clip-content-text" title={text}>
226+
{text}
227+
</div>
228+
);
229229
}

src/storage.ts

+13-20
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,6 @@ export const storage = {
7272
if (config.alwaysOnTop != null) {
7373
getWindow().setAlwaysOnTop(config.alwaysOnTop);
7474
}
75-
if (config.limit != null) {
76-
applyLengthLimit();
77-
}
7875
},
7976

8077
getLists(): List[] {
@@ -113,15 +110,7 @@ export const storage = {
113110
addNewItem(item: ClipItem) {
114111
_clipboardItems.unshift(item);
115112
writeClipItemToDisk(item);
116-
applyLengthLimit();
117-
},
118-
119-
addExistingItem(item: ClipItem) {
120-
const index = _clipboardItems.findIndex((i) => i.id === item.id);
121-
if (index > -1) {
122-
_clipboardItems.splice(index, 1);
123-
}
124-
_clipboardItems.unshift(item);
113+
applySizeLimit();
125114
},
126115

127116
replaceItems(items: ClipItem[]) {
@@ -186,14 +175,18 @@ function getFilePath(item: ClipItem) {
186175
return path.join(clipItemsDir, `${item.id}.json`);
187176
}
188177

189-
function applyLengthLimit() {
190-
while (_clipboardItems.length > _state.config.limit) {
191-
const index = _clipboardItems.findLastIndex((i) => i.list == null);
192-
// Index 0 is the most recent item, so we don't want to remove that.
193-
if (index < 1) {
194-
break;
178+
function applySizeLimit() {
179+
let index = _clipboardItems.length - 1;
180+
181+
// Index 0 is the most recent item and we don't want to remove that.
182+
while (index > 0 && _clipboardItems.length > _state.config.limit) {
183+
const item = _clipboardItems[index];
184+
185+
if (item.list == null) {
186+
deleteClipItemFromDisk(item);
187+
_clipboardItems.splice(index, 1);
195188
}
196-
const removedItem = _clipboardItems.splice(index, 1)[0];
197-
deleteClipItemFromDisk(removedItem);
189+
190+
--index;
198191
}
199192
}

src/types/types.ts

+12-11
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,24 @@ export type Visibility =
1717
// Hide window if not pinned. If pinned, blur window
1818
| "hideOrBlurIfPinned";
1919

20-
export interface ClipItemMeta {
21-
readonly src?: string;
22-
readonly alt?: string;
20+
export interface ClipItemImage {
21+
readonly src: string | undefined;
22+
readonly alt: string | undefined;
23+
readonly data: string;
2324
}
2425

2526
export interface ClipItem {
2627
readonly id: string;
2728
readonly created: number;
29+
readonly hash: string;
2830
readonly type: ClipItemType;
29-
name?: string;
30-
list?: string;
31-
readonly text?: string;
32-
readonly rtf?: string;
33-
readonly html?: string;
34-
readonly image?: string;
35-
readonly bookmark?: ReadBookmark;
36-
readonly meta?: ClipItemMeta;
31+
name: string | undefined;
32+
list: string | undefined;
33+
readonly text: string | undefined;
34+
readonly rtf: string | undefined;
35+
readonly html: string | undefined;
36+
readonly bookmark: ReadBookmark | undefined;
37+
readonly image: ClipItemImage | undefined;
3738
}
3839

3940
export interface Config {

src/util/clipItemId.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
let lastTimestamp = 0;
2+
let count = 0;
3+
4+
// Serializes timestamp an adds count if needed to be unique
5+
export function getNextClipItemId(timestamp: number): string {
6+
if (timestamp !== lastTimestamp) {
7+
lastTimestamp = timestamp;
8+
count = 1;
9+
return timestamp.toString();
10+
}
11+
12+
return `${timestamp}-${count++}`;
13+
}
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import crypto from "node:crypto";
22

3-
export function hash(str: string): string {
3+
export function createHash(str: string): string {
44
return crypto.createHash("sha256").update(str).digest("hex");
55
}

src/util/filterItems.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function applySearchFilters(items: ClipItem[], isVisible: boolean): ClipI
3131
(item.text ?? item.rtf)?.toLowerCase().includes(searchText) ||
3232
item.html?.toLowerCase().includes(searchText) ||
3333
item.bookmark?.title?.toLowerCase().includes(searchText) ||
34-
item.meta?.alt?.toLowerCase().includes(searchText),
34+
item.image?.alt?.toLowerCase().includes(searchText),
3535
);
3636
}
3737
}

0 commit comments

Comments
 (0)