Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | 45x 580x 31x 31x 9x 31x 31x 580x 580x 579x 579x 579x 579x 579x 579x 1x 114x 114x 114x 114x 114x 114x 114x 114x 114x 114x 105x 105x 105x 105x 105x 45x 79x 191x 191x 51x 45x 34x 36x 45x 4x 6x 167x | export type CaptureMode =
'visible' | 'full' | 'selection' | 'tabs' | 'screen' | 'window';
/** Live capture state shared via chrome.storage.session. `value` is 0..1. */
export interface Progress {
mode: CaptureMode;
value: number;
detail: string;
}
export interface Rect {
x: number;
y: number;
w: number;
h: number;
}
export interface Shape {
id: string;
kind:
| 'arrow'
| 'rect'
| 'ellipse'
| 'line'
| 'pen'
| 'highlight'
| 'text'
| 'step'
| 'blur';
color: string;
size: number;
/** Flat [x, y, x, y, ...] list in original image pixels. */
pts: number[];
text?: string;
}
export interface Edits {
shapes: Shape[];
crop: Rect | null;
}
export interface Shot {
id: string;
createdAt: number;
updatedAt: number;
title: string;
url: string;
mode: CaptureMode;
width: number;
height: number;
size: number;
/** Rendered PNG (edits applied). */
blob: Blob;
thumb: Blob;
/** Unedited capture, kept so edits stay re-editable. */
original?: Blob;
edits?: Edits;
}
const STORE = 'shots';
let dbPromise: Promise<IDBDatabase> | undefined;
function openDb() {
dbPromise ??= new Promise((resolve, reject) => {
const req = indexedDB.open('daguerre', 1);
req.onupgradeneeded = () =>
req.result.createObjectStore(STORE, { keyPath: 'id' });
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
return dbPromise;
}
async function run<T>(
mode: IDBTransactionMode,
fn: (store: IDBObjectStore) => IDBRequest<T> | void,
): Promise<T> {
const db = await openDb();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE, mode);
const req = fn(tx.objectStore(STORE));
tx.oncomplete = () => resolve(req?.result as T);
tx.onerror = () => reject(tx.error);
tx.onabort = () =>
reject(tx.error ?? new Error('Storage transaction aborted'));
});
}
export async function makeThumb(source: Blob) {
const bmp = await createImageBitmap(source);
const w = Math.min(480, bmp.width);
// Tall full-page shots get a top crop instead of a sliver.
const h = Math.max(
1,
Math.min(Math.round(w * 0.625), Math.round((bmp.height * w) / bmp.width)),
);
const canvas = new OffscreenCanvas(w, h);
const ctx = canvas.getContext('2d')!;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(bmp, 0, 0, bmp.width, (h * bmp.width) / w, 0, 0, w, h);
const result = {
thumb: await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.82 }),
width: bmp.width,
height: bmp.height,
};
bmp.close();
return result;
}
export async function addShot(
blob: Blob,
meta: Pick<Shot, 'title' | 'url' | 'mode'>,
) {
const { thumb, width, height } = await makeThumb(blob);
const now = Date.now();
const shot: Shot = {
id: crypto.randomUUID(),
createdAt: now,
updatedAt: now,
...meta,
width,
height,
size: blob.size,
blob,
thumb,
};
await run('readwrite', (s) => s.put(shot));
return shot.id;
}
export const getShot = (id: string) =>
run<Shot | undefined>('readonly', (s) => s.get(id));
export const listShots = async () =>
(await run<Shot[]>('readonly', (s) => s.getAll())).sort(
(a, b) => b.createdAt - a.createdAt,
);
export const putShots = (shots: Shot[]) =>
run('readwrite', (s) => {
for (const shot of shots) s.put(shot);
});
export const deleteShots = (ids: string[]) =>
run('readwrite', (s) => {
for (const id of ids) s.delete(id);
});
export const clearShots = () => run('readwrite', (s) => s.clear());
|