@medeo-plugin/plugin-screenshot 1.0.0 → 1.0.2

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.
Files changed (76) hide show
  1. package/README.md +8 -5
  2. package/package.json +18 -5
  3. package/plugin.medeoplugin +0 -0
  4. package/src/README.md +28 -0
  5. package/src/README.zh-CN.md +20 -0
  6. package/src/about.json +7 -0
  7. package/src/icon.svg +1 -0
  8. package/src/locales/en.json +141 -0
  9. package/src/locales/zh-CN.json +141 -0
  10. package/src/locales/zh-TW.json +141 -0
  11. package/src/manifest.json +125 -0
  12. package/src/media/en/editor.png +0 -0
  13. package/src/media/en/settings.png +0 -0
  14. package/src/media/zh-CN/editor.png +0 -0
  15. package/src/media/zh-CN/settings.png +0 -0
  16. package/src/resources/helper.js +99 -0
  17. package/src/resources/trayTemplate.png +0 -0
  18. package/src/resources/trayTemplate.svg +1 -0
  19. package/src/src/annotations.ts +398 -0
  20. package/src/src/beautify.ts +41 -0
  21. package/src/src/geometry.ts +79 -0
  22. package/src/src/naming.ts +19 -0
  23. package/src/src/protocol.ts +53 -0
  24. package/src/src/settings.ts +48 -0
  25. package/src/src/shortcuts.ts +25 -0
  26. package/src/src/ui/overlay.ts +874 -0
  27. package/src/src/ui/settings.ts +140 -0
  28. package/src/tests/logic.test.mjs +101 -0
  29. package/src/ui/entry.ts +4 -0
  30. package/src/ui/icons/align-center.svg +1 -0
  31. package/src/ui/icons/align-left.svg +1 -0
  32. package/src/ui/icons/align-right.svg +1 -0
  33. package/src/ui/icons/arrow-double.svg +1 -0
  34. package/src/ui/icons/arrow-open.svg +1 -0
  35. package/src/ui/icons/arrow-single.svg +1 -0
  36. package/src/ui/icons/arrow-tail.svg +1 -0
  37. package/src/ui/icons/arrow-thick.svg +1 -0
  38. package/src/ui/icons/arrow.svg +1 -0
  39. package/src/ui/icons/beautify.svg +1 -0
  40. package/src/ui/icons/bold.svg +1 -0
  41. package/src/ui/icons/censor.svg +1 -0
  42. package/src/ui/icons/close.svg +1 -0
  43. package/src/ui/icons/copy-doc.svg +1 -0
  44. package/src/ui/icons/copy.svg +1 -0
  45. package/src/ui/icons/ellipse.svg +1 -0
  46. package/src/ui/icons/fill-fill.svg +1 -0
  47. package/src/ui/icons/fill-stroke.svg +1 -0
  48. package/src/ui/icons/fill-strokeFill.svg +1 -0
  49. package/src/ui/icons/highlight.svg +1 -0
  50. package/src/ui/icons/invert.svg +1 -0
  51. package/src/ui/icons/italic.svg +1 -0
  52. package/src/ui/icons/line-dashed.svg +1 -0
  53. package/src/ui/icons/line-dotted.svg +1 -0
  54. package/src/ui/icons/line-solid.svg +1 -0
  55. package/src/ui/icons/line.svg +1 -0
  56. package/src/ui/icons/marker.svg +1 -0
  57. package/src/ui/icons/measure.svg +1 -0
  58. package/src/ui/icons/number.svg +1 -0
  59. package/src/ui/icons/ocr.svg +1 -0
  60. package/src/ui/icons/open.svg +1 -0
  61. package/src/ui/icons/pencil.svg +1 -0
  62. package/src/ui/icons/rect.svg +1 -0
  63. package/src/ui/icons/redo.svg +1 -0
  64. package/src/ui/icons/sampler.svg +1 -0
  65. package/src/ui/icons/save.svg +1 -0
  66. package/src/ui/icons/select.svg +1 -0
  67. package/src/ui/icons/stamp.svg +1 -0
  68. package/src/ui/icons/text-bg.svg +1 -0
  69. package/src/ui/icons/text.svg +1 -0
  70. package/src/ui/icons/trash.svg +1 -0
  71. package/src/ui/icons/underline.svg +1 -0
  72. package/src/ui/icons/undo.svg +1 -0
  73. package/src/ui/index.html +12 -0
  74. package/src/ui/settings.html +5 -0
  75. package/src/ui/style.css +105 -0
  76. package/src/worker.ts +285 -0
@@ -0,0 +1,53 @@
1
+ // Messages between the worker and the two windows (overlay editor, settings). The channel
2
+ // broadcasts to every window of the plugin, so each message names who it is for.
3
+ import type { Rect } from './geometry.ts';
4
+ import type { Settings, ShortcutKey } from './settings.ts';
5
+
6
+ /** area: select then edit · quick: save/copy as soon as the selection is made · ocr: read text · full: whole display. */
7
+ export type Mode = 'area' | 'quick' | 'ocr' | 'full';
8
+ export type Action = 'copy' | 'save' | 'ocr';
9
+ export interface Output { format: 'png' | 'jpeg'; downscale: boolean; enter: 'copy' | 'save' }
10
+
11
+ /** One captured display: `width`/`height` in points, the PNG is `width*scale` pixels wide. */
12
+ export interface Shot { seq: number; mode: Mode; width: number; height: number; scale: number; windows: Rect[]; output: Output }
13
+ export interface OcrResult { text: string; codes: string[] }
14
+
15
+ export type ToOverlay =
16
+ | { type: 'shot'; shot: Shot; parts: number }
17
+ | { type: 'chunk'; seq: number; i: number; data: string }
18
+ | { type: 'ocr'; seq: number; result: OcrResult | null }
19
+ | { type: 'overlay-toast'; key: string; params?: Record<string, string> };
20
+
21
+ export type FromOverlay =
22
+ | { type: 'overlay-ready' }
23
+ | { type: 'result'; seq: number; action: Action; mime: string; i: number; parts: number; data: string }
24
+ | { type: 'cancel'; seq: number }
25
+ | { type: 'copy-text'; text: string }
26
+ | { type: 'open-url'; url: string };
27
+
28
+ export interface SettingsState { settings: Settings; saveDir: string; errors: Partial<Record<ShortcutKey, string>> }
29
+
30
+ export type ToSettings = { type: 'settings'; state: SettingsState } | { type: 'toast'; tone: 'success' | 'error'; key: string };
31
+
32
+ export type FromSettings =
33
+ | { type: 'settings-ready' }
34
+ | { type: 'set'; patch: Partial<Settings> }
35
+ | { type: 'choose-folder' }
36
+ | { type: 'reveal-folder' }
37
+ | { type: 'open-privacy' }
38
+ | { type: 'capture' };
39
+
40
+ /** Under the 1 MiB channel limit with room for the envelope. */
41
+ export const CHUNK = 768 * 1024;
42
+ export const split = (b64: string) => { const out: string[] = []; for (let i = 0; i < b64.length; i += CHUNK) out.push(b64.slice(i, i + CHUNK)); return out.length ? out : ['']; };
43
+
44
+ /** Vision observations (normalized, bottom-left origin) → reading order, one text line per row. */
45
+ export function joinLines(lines: { t: string; x: number; y: number; h: number }[]): string {
46
+ const sorted = [...lines].sort((a, b) => (b.y + b.h / 2) - (a.y + a.h / 2));
47
+ const rows: { y: number; h: number; items: typeof lines }[] = [];
48
+ for (const l of sorted) {
49
+ const mid = l.y + l.h / 2, row = rows.find(r => Math.abs(r.y - mid) < Math.min(r.h, l.h) * 0.5);
50
+ if (row) row.items.push(l); else rows.push({ y: mid, h: l.h, items: [l] });
51
+ }
52
+ return rows.map(r => r.items.sort((a, b) => a.x - b.x).map(i => i.t).join(' ')).join('\n');
53
+ }
@@ -0,0 +1,48 @@
1
+ // Settings live in the plugin's private storagePath/settings.json; bad values fall back to defaults.
2
+ import { valid } from './shortcuts.ts';
3
+
4
+ export const SHORTCUTS = ['shortcutArea', 'shortcutFull', 'shortcutQuick', 'shortcutOcr'] as const;
5
+ export type ShortcutKey = typeof SHORTCUTS[number];
6
+
7
+ export interface Settings {
8
+ /** Accelerators; null = no shortcut. Only area capture has one by default (⌘⇧X). */
9
+ shortcutArea: string | null;
10
+ shortcutFull: string | null;
11
+ shortcutQuick: string | null;
12
+ shortcutOcr: string | null;
13
+ /** What ↩ (and quick capture) does. */
14
+ enterAction: 'copy' | 'save';
15
+ format: 'png' | 'jpeg';
16
+ /** Save Retina captures at 1× (half the pixels). */
17
+ downscale: boolean;
18
+ /** `~/…` or absolute. */
19
+ saveDir: string;
20
+ openAfterSave: boolean;
21
+ }
22
+ export const DEFAULTS: Settings = {
23
+ shortcutArea: 'Command+Shift+X', shortcutFull: null, shortcutQuick: null, shortcutOcr: null,
24
+ enterAction: 'copy', format: 'png', downscale: false, saveDir: '~/Pictures/Medeo Screenshots', openAfterSave: false,
25
+ };
26
+
27
+ const shortcut = (v: unknown, d: string | null) => v === null ? null : typeof v === 'string' && valid(v) ? v : d;
28
+
29
+ export function normalize(value: unknown): Settings {
30
+ const v = value && typeof value === 'object' ? value as Record<string, unknown> : {};
31
+ const out = { ...DEFAULTS };
32
+ // Earlier versions stored a single capture shortcut.
33
+ const legacy = Object.hasOwn(v, 'shortcutCapture') && !Object.hasOwn(v, 'shortcutArea') ? { shortcutArea: v.shortcutCapture } : {};
34
+ const all = { ...v, ...legacy } as Record<string, unknown>;
35
+ const seen = new Set<string>();
36
+ for (const k of SHORTCUTS) {
37
+ const s = Object.hasOwn(all, k) ? shortcut(all[k], DEFAULTS[k]) : DEFAULTS[k];
38
+ // One key never does two things: a later duplicate is dropped.
39
+ out[k] = s && seen.has(s.toLowerCase()) ? null : s;
40
+ if (out[k]) seen.add(out[k]!.toLowerCase());
41
+ }
42
+ if (v.enterAction === 'copy' || v.enterAction === 'save') out.enterAction = v.enterAction;
43
+ if (v.format === 'png' || v.format === 'jpeg') out.format = v.format;
44
+ if (typeof v.downscale === 'boolean') out.downscale = v.downscale;
45
+ if (typeof v.saveDir === 'string' && v.saveDir.trim() && v.saveDir.length < 1024) out.saveDir = v.saveDir.trim();
46
+ if (typeof v.openAfterSave === 'boolean') out.openAfterSave = v.openAfterSave;
47
+ return out;
48
+ }
@@ -0,0 +1,25 @@
1
+ // Recording keys into Electron accelerators and showing them (after Window Ring). No SDK.
2
+ const MODIFIERS = ['Control', 'Alt', 'Shift', 'Command'] as const;
3
+ const SYMBOLS: Record<string, string> = { Control: '⌃', Alt: '⌥', Shift: '⇧', Command: '⌘', Left: '←', Right: '→', Up: '↑', Down: '↓', Enter: '↩', Space: 'Space', Backspace: '⌫', Tab: '⇥' };
4
+ const PUNCTUATION: Record<string, string> = { Minus: '-', Equal: '=', BracketLeft: '[', BracketRight: ']', Semicolon: ';', Quote: "'", Comma: ',', Period: '.', Slash: '/', Backslash: '\\', Backquote: '`' };
5
+ const NAMED: Record<string, string> = { ArrowLeft: 'Left', ArrowRight: 'Right', ArrowUp: 'Up', ArrowDown: 'Down', Enter: 'Enter', NumpadEnter: 'Enter', Space: 'Space', Backspace: 'Backspace', Tab: 'Tab' };
6
+
7
+ const canonical = (accelerator: string) => { const parts = accelerator.split('+'), key = parts.pop()!; return [...MODIFIERS.filter(m => parts.includes(m)), key].join('+'); };
8
+ export const same = (a: string, b: string) => canonical(a).toLowerCase() === canonical(b).toLowerCase();
9
+
10
+ /** One non-modifier key plus ⌃, ⌥ or ⌘ (⇧ alone would eat ordinary typing); F-keys may stand alone. */
11
+ export function valid(accelerator: string): boolean {
12
+ const parts = accelerator.split('+'), key = parts.pop();
13
+ if (!key || (MODIFIERS as readonly string[]).includes(key)) return false;
14
+ if (parts.some(p => !(MODIFIERS as readonly string[]).includes(p)) || new Set(parts).size !== parts.length) return false;
15
+ return /^F(?:[1-9]|1\d|20)$/.test(key) || parts.some(p => p !== 'Shift');
16
+ }
17
+
18
+ export function fromKey(e: { code: string; ctrlKey: boolean; altKey: boolean; shiftKey: boolean; metaKey: boolean }): string | null {
19
+ const m = /^(?:Key([A-Z])|Digit(\d)|(F(?:[1-9]|1\d|20)))$/.exec(e.code);
20
+ const key = m ? (m[1] ?? m[2] ?? m[3]) : NAMED[e.code] ?? PUNCTUATION[e.code];
21
+ if (!key) return null;
22
+ return [...[e.ctrlKey && 'Control', e.altKey && 'Alt', e.shiftKey && 'Shift', e.metaKey && 'Command'].filter(Boolean) as string[], key].join('+');
23
+ }
24
+
25
+ export const symbols = (accelerator: string) => canonical(accelerator).split('+').map(p => SYMBOLS[p] ?? p).join('');