copyhub-cli 1.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ui/preload.cjs CHANGED
@@ -2,7 +2,10 @@ const { contextBridge, ipcRenderer } = require('electron');
2
2
 
3
3
  contextBridge.exposeInMainWorld('copyhub', {
4
4
  getMeta: () => ipcRenderer.invoke('overlay:meta'),
5
- getHistory: () => ipcRenderer.invoke('history:get'),
5
+ /** @param {{ page?: number, pageSize?: number, refresh?: boolean }} [opts] */
6
+ getHistory: (opts) => ipcRenderer.invoke('history:get', opts ?? {}),
7
+ /** Local history only — instant; used while Sheet sync runs. */
8
+ getHistoryLocal: (opts) => ipcRenderer.invoke('history:getLocal', opts ?? {}),
6
9
  copyPick: (text) => ipcRenderer.invoke('history:copy', text),
7
10
  onOpen: (fn) => {
8
11
  ipcRenderer.on('overlay:open', (_e) => {
@@ -58,6 +58,53 @@
58
58
  cursor: grabbing;
59
59
  }
60
60
 
61
+ #list-wrap {
62
+ flex: 1;
63
+ min-height: 0;
64
+ display: flex;
65
+ flex-direction: column;
66
+ position: relative;
67
+ }
68
+
69
+ #list-loading {
70
+ flex-shrink: 0;
71
+ display: none;
72
+ align-items: center;
73
+ gap: 10px;
74
+ padding: 8px 14px;
75
+ font-size: 12px;
76
+ font-weight: 600;
77
+ color: var(--accent);
78
+ background: linear-gradient(90deg, var(--accent-soft), rgba(61, 90, 254, 0.02));
79
+ border-bottom: 1px solid var(--border);
80
+ -webkit-app-region: no-drag;
81
+ app-region: no-drag;
82
+ animation: hint-pulse 1.4s ease-in-out infinite;
83
+ }
84
+
85
+ #list-loading.visible {
86
+ display: flex;
87
+ }
88
+
89
+ #list-loading .spinner {
90
+ width: 16px;
91
+ height: 16px;
92
+ flex-shrink: 0;
93
+ border: 2px solid rgba(61, 90, 254, 0.35);
94
+ border-top-color: var(--accent);
95
+ border-radius: 50%;
96
+ animation: spin 0.65s linear infinite;
97
+ }
98
+
99
+ @keyframes spin {
100
+ to { transform: rotate(360deg); }
101
+ }
102
+
103
+ @keyframes hint-pulse {
104
+ 0%, 100% { opacity: 1; }
105
+ 50% { opacity: 0.82; }
106
+ }
107
+
61
108
  #list {
62
109
  flex: 1;
63
110
  min-height: 0;
@@ -173,33 +220,173 @@
173
220
  border-radius: var(--radius);
174
221
  font-size: 13px;
175
222
  }
223
+
224
+ #pager {
225
+ flex-shrink: 0;
226
+ display: none;
227
+ align-items: center;
228
+ justify-content: center;
229
+ gap: 12px;
230
+ padding: 8px 12px 12px;
231
+ border-top: 1px solid var(--border);
232
+ background: linear-gradient(180deg, var(--bg) 0%, #fafbfc 100%);
233
+ -webkit-app-region: no-drag;
234
+ app-region: no-drag;
235
+ }
236
+
237
+ #pager.visible {
238
+ display: flex;
239
+ }
240
+
241
+ #pager button {
242
+ font: inherit;
243
+ font-size: 13px;
244
+ font-weight: 600;
245
+ padding: 6px 14px;
246
+ border-radius: 8px;
247
+ border: 1px solid var(--border);
248
+ background: var(--surface);
249
+ color: var(--text);
250
+ cursor: pointer;
251
+ transition: background 0.15s, border-color 0.15s;
252
+ }
253
+
254
+ #pager button:hover:not(:disabled) {
255
+ background: var(--surface-hover);
256
+ border-color: rgba(61, 90, 254, 0.35);
257
+ }
258
+
259
+ #pager button:disabled {
260
+ opacity: 0.45;
261
+ cursor: default;
262
+ }
263
+
264
+ #pg-info {
265
+ font-size: 12px;
266
+ color: var(--text-muted);
267
+ min-width: 0;
268
+ text-align: center;
269
+ flex: 1;
270
+ }
271
+
272
+ .sheet-hint {
273
+ flex-shrink: 0;
274
+ font-size: 11px;
275
+ line-height: 1.45;
276
+ color: var(--text-muted);
277
+ padding: 6px 14px 4px;
278
+ text-align: center;
279
+ border-top: 1px solid var(--border);
280
+ background: var(--bg);
281
+ -webkit-app-region: no-drag;
282
+ app-region: no-drag;
283
+ }
284
+
285
+ .sheet-hint[hidden] {
286
+ display: none !important;
287
+ }
176
288
  </style>
177
289
  </head>
178
290
  <body>
179
291
  <div id="drag-strip" title="Drag to move window"></div>
180
- <div id="list"></div>
292
+ <div id="list-wrap">
293
+ <div id="list-loading" aria-live="polite">
294
+ <div class="spinner" aria-hidden="true"></div>
295
+ <span>Syncing Google Sheet…</span>
296
+ </div>
297
+ <div id="list"></div>
298
+ </div>
299
+ <div id="sheet-hint" class="sheet-hint" hidden></div>
300
+ <div id="pager" aria-label="Pagination">
301
+ <button type="button" id="pg-prev">Previous</button>
302
+ <span id="pg-info"></span>
303
+ <button type="button" id="pg-next">Next</button>
304
+ </div>
181
305
  <script>
182
306
  const listEl = document.getElementById('list');
307
+ const listLoadingEl = document.getElementById('list-loading');
308
+ const pagerEl = document.getElementById('pager');
309
+ const pgPrev = document.getElementById('pg-prev');
310
+ const pgNext = document.getElementById('pg-next');
311
+ const pgInfo = document.getElementById('pg-info');
312
+ const sheetHintEl = document.getElementById('sheet-hint');
313
+
314
+ const pageSize = 10;
315
+ let currentPage = 1;
316
+ /** @type {{ total: number, totalPages: number }} */
317
+ let lastPaging = { total: 0, totalPages: 1 };
318
+ let lastSheetHasMore = false;
319
+ let sheetSyncInFlight = false;
183
320
 
184
- async function refresh() {
321
+ function setLoadingUi(on) {
322
+ listLoadingEl.classList.toggle('visible', on);
323
+ }
324
+
325
+ function syncPagerBar() {
326
+ const { total, totalPages } = lastPaging;
327
+ if (!total) return;
328
+ pgPrev.disabled = sheetSyncInFlight || currentPage <= 1;
329
+ pgNext.disabled =
330
+ sheetSyncInFlight ||
331
+ (currentPage * pageSize >= total && !lastSheetHasMore);
332
+ const from = (currentPage - 1) * pageSize + 1;
333
+ const to = Math.min(currentPage * pageSize, total);
334
+ const moreMark = lastSheetHasMore && currentPage * pageSize >= total ? '+' : '';
335
+ const pagesShown =
336
+ lastSheetHasMore && currentPage * pageSize >= total
337
+ ? Math.max(totalPages, currentPage + 1)
338
+ : totalPages;
339
+ pgInfo.textContent = `Page ${currentPage} / ${pagesShown} · ${from}–${to} of ${total}${moreMark}`;
340
+ }
341
+
342
+ function renderHistoryResponse(res) {
185
343
  listEl.innerHTML = '';
186
- const res = await window.copyhub.getHistory();
344
+
187
345
  if (res && res.error) {
346
+ pagerEl.classList.remove('visible');
347
+ sheetHintEl.hidden = true;
188
348
  const e = document.createElement('div');
189
349
  e.id = 'err';
190
350
  e.textContent = res.error;
191
351
  listEl.appendChild(e);
352
+ lastPaging = { total: 0, totalPages: 1 };
353
+ lastSheetHasMore = false;
192
354
  return;
193
355
  }
356
+
357
+ lastSheetHasMore = Boolean(res.sheetHasMore);
358
+ currentPage = res.page ?? currentPage;
194
359
  const rows = res && Array.isArray(res.items) ? res.items : [];
195
- if (!rows.length) {
360
+ const total = res.total ?? 0;
361
+ const totalPages = res.totalPages ?? 1;
362
+ lastPaging = { total, totalPages };
363
+
364
+ if (!total) {
365
+ pagerEl.classList.remove('visible');
366
+ if (res.sheetHint) {
367
+ sheetHintEl.hidden = false;
368
+ sheetHintEl.textContent = res.sheetHint;
369
+ } else {
370
+ sheetHintEl.hidden = true;
371
+ }
196
372
  const d = document.createElement('div');
197
373
  d.id = 'empty';
198
374
  d.textContent =
199
- 'No history yet — run copyhub start and copy something. Click a row to copy again; click outside or Esc to close.';
375
+ 'No local history yet — run copyhub start and copy something. Click a row to copy again; click outside or Esc to close.';
200
376
  listEl.appendChild(d);
201
377
  return;
202
378
  }
379
+
380
+ if (res.sheetHint) {
381
+ sheetHintEl.hidden = false;
382
+ sheetHintEl.textContent = res.sheetHint;
383
+ } else {
384
+ sheetHintEl.hidden = true;
385
+ }
386
+
387
+ pagerEl.classList.add('visible');
388
+ syncPagerBar();
389
+
203
390
  for (const r of rows) {
204
391
  const div = document.createElement('div');
205
392
  div.className = 'row' + (r.synced ? ' synced' : '');
@@ -215,10 +402,72 @@
215
402
  }
216
403
  }
217
404
 
405
+ async function loadHistory(opt = {}) {
406
+ const refreshCache = Boolean(opt.refreshCache);
407
+ if (refreshCache) currentPage = 1;
408
+ if (typeof opt.page === 'number' && opt.page >= 1) {
409
+ currentPage = opt.page;
410
+ }
411
+
412
+ if (refreshCache) {
413
+ sheetSyncInFlight = true;
414
+ setLoadingUi(true);
415
+ pgPrev.disabled = true;
416
+ pgNext.disabled = true;
417
+
418
+ const quick = await window.copyhub.getHistoryLocal({
419
+ page: currentPage,
420
+ pageSize,
421
+ });
422
+
423
+ if (quick.error) {
424
+ sheetSyncInFlight = false;
425
+ setLoadingUi(false);
426
+ renderHistoryResponse(quick);
427
+ return;
428
+ }
429
+
430
+ currentPage = quick.page ?? currentPage;
431
+ renderHistoryResponse(quick);
432
+
433
+ try {
434
+ const full = await window.copyhub.getHistory({
435
+ page: currentPage,
436
+ pageSize,
437
+ refresh: true,
438
+ });
439
+ currentPage = full.page ?? currentPage;
440
+ renderHistoryResponse(full);
441
+ } finally {
442
+ sheetSyncInFlight = false;
443
+ setLoadingUi(false);
444
+ syncPagerBar();
445
+ }
446
+ return;
447
+ }
448
+
449
+ const res = await window.copyhub.getHistory({
450
+ page: currentPage,
451
+ pageSize,
452
+ refresh: false,
453
+ });
454
+ renderHistoryResponse(res);
455
+ }
456
+
457
+ pgPrev.addEventListener('click', () => {
458
+ if (sheetSyncInFlight || currentPage <= 1) return;
459
+ void loadHistory({ page: currentPage - 1 });
460
+ });
461
+
462
+ pgNext.addEventListener('click', () => {
463
+ if (sheetSyncInFlight) return;
464
+ void loadHistory({ page: currentPage + 1 });
465
+ });
466
+
218
467
  window.copyhub.onOpen(() => {
219
- void refresh();
468
+ void loadHistory({ refreshCache: true });
220
469
  });
221
- void refresh();
222
470
  </script>
223
471
  </body>
224
472
  </html>
473
+