sdfsdf
This commit is contained in:
1 parent
a59be0fc9c
commit
4cd68793c9
41 files changed
+287
-129
No files matched your search
+85
-39
@@ -1,72 +1,118 @@
|
||||
'use strict';
|
||||
// Runs the real Ink app against a seeded demo vault and saves 16:10 screenshots.
|
||||
// electron scripts/capture/capture.cjs --ozone-platform=headless --force-device-scale-factor=1.5
|
||||
// Env: INK_APP (path to the Ink repo, read only), OUT (output dir), THEMES (comma list), SCENES (comma list)
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
// Runs the packaged Ink app (the released AppImage, extracted) against a seeded demo vault and saves 16:10
|
||||
// screenshots, driving it over the Chrome DevTools protocol. Nothing is read from the app's source.
|
||||
// node scripts/capture/capture.cjs (env: INK_BIN, OUT, THEME, SCENES; run.sh sets these up)
|
||||
const { spawn } = require('node:child_process');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const INK = process.env.INK_APP || path.resolve(__dirname, '../../../../App/Ink');
|
||||
const BIN = process.env.INK_BIN;
|
||||
const OUT = process.env.OUT || path.resolve(__dirname, '../../src/assets/screens');
|
||||
const THEMES = (process.env.THEMES || 'dark,light').split(',');
|
||||
const ONLY = process.env.SCENES ? process.env.SCENES.split(',') : null;
|
||||
const theme = process.env.THEME || 'dark';
|
||||
const palette = process.env.PALETTE || 'default';
|
||||
const accent = process.env.ACCENT || 'blush';
|
||||
const SUFFIX = process.env.SUFFIX || theme;
|
||||
const { seedVault } = require('./seed-vault.cjs');
|
||||
const scenes = require('./scenes.cjs');
|
||||
if (!BIN) throw new Error('Set INK_BIN to the extracted AppImage\'s `ink` binary (run.sh does this).');
|
||||
|
||||
const W = 1600, H = 1000;
|
||||
const W = 1600, H = 1000, SCALE = 1.5, PORT = 9300 + (process.pid % 500);
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const theme = process.env.THEME || THEMES[0];
|
||||
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ink-shots-'));
|
||||
const vault = path.join(tmp, 'Notes');
|
||||
fs.mkdirSync(vault);
|
||||
const seeded = seedVault(vault);
|
||||
|
||||
// No network: a link card gets a fixed title and description instead of fetching.
|
||||
const linkpreview = require(path.join(INK, 'electron/linkpreview.cjs'));
|
||||
linkpreview.hooks.lookup = async () => [{ address: '93.184.216.34' }];
|
||||
linkpreview.hooks.fetch = async () => new Response(
|
||||
'<html><head><title>JSON Canvas</title><meta property="og:title" content="JSON Canvas"><meta name="description" content="An open file format for infinite canvas data.">' +
|
||||
'</head><body></body></html>', { status: 200, headers: { 'content-type': 'text/html' } });
|
||||
|
||||
app.setPath('userData', tmp);
|
||||
fs.writeFileSync(path.join(tmp, 'settings.json'), JSON.stringify({
|
||||
vault, lastPage: seeded.lastPage, theme, bounds: { width: W, height: H },
|
||||
vault, lastPage: seeded.lastPage, theme, palette, accent, bounds: { width: W, height: H },
|
||||
fontSize: 16, outline: false, sidebarCollapsed: false, tagsOpen: true,
|
||||
vaults: { [vault]: seeded.vaultSettings },
|
||||
}));
|
||||
app.commandLine.appendSwitch('lang', 'en-US');
|
||||
require(path.join(INK, 'electron/main.cjs'));
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
let win;
|
||||
for (let i = 0; i < 100 && !(win = BrowserWindow.getAllWindows()[0]); i++) await sleep(100);
|
||||
win.setContentSize(W, H);
|
||||
const wc = win.webContents;
|
||||
if (wc.isLoading()) await new Promise((r) => wc.once('did-finish-load', r));
|
||||
const js = (code) => wc.executeJavaScript(`(async () => { ${code} })()`);
|
||||
const key = async (keyCode, modifiers = []) => { wc.sendInputEvent({ type: 'keyDown', keyCode, modifiers }); wc.sendInputEvent({ type: 'keyUp', keyCode, modifiers }); await sleep(250); };
|
||||
const type = async (t, d = 45) => { for (const ch of t) { wc.sendInputEvent({ type: 'char', keyCode: ch }); await sleep(d); } await sleep(200); };
|
||||
const click = async (x, y) => { wc.sendInputEvent({ type: 'mouseMove', x, y }); wc.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount: 1 }); wc.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount: 1 }); await sleep(250); };
|
||||
const KEYS = { Escape: ['Escape', 27], Return: ['Enter', 13], Enter: ['Enter', 13], Tab: ['Tab', 9], Backspace: ['Backspace', 8], Down: ['ArrowDown', 40], Up: ['ArrowUp', 38] };
|
||||
const MODS = { alt: 1, control: 2, meta: 4, shift: 8 };
|
||||
|
||||
async function connect() {
|
||||
for (let i = 0; i < 150; i++) {
|
||||
try {
|
||||
const list = await (await fetch(`http://127.0.0.1:${PORT}/json`)).json();
|
||||
const page = list.find((t) => t.type === 'page' && !t.url.startsWith('devtools'));
|
||||
if (page) {
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; });
|
||||
return ws;
|
||||
}
|
||||
} catch {}
|
||||
await sleep(200);
|
||||
}
|
||||
throw new Error('Ink did not start');
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const proc = spawn(BIN, [
|
||||
`--user-data-dir=${tmp}`, `--remote-debugging-port=${PORT}`, '--ozone-platform=headless', '--no-sandbox', '--lang=en-US',
|
||||
], { stdio: 'ignore' });
|
||||
const ws = await connect();
|
||||
let id = 0;
|
||||
const pending = new Map();
|
||||
ws.onmessage = (m) => {
|
||||
const d = JSON.parse(m.data);
|
||||
if (d.id && pending.has(d.id)) { const { res, rej } = pending.get(d.id); pending.delete(d.id); d.error ? rej(new Error(d.error.message)) : res(d.result); }
|
||||
};
|
||||
const send = (method, params = {}) => new Promise((res, rej) => { pending.set(++id, { res, rej }); ws.send(JSON.stringify({ id, method, params })); });
|
||||
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: W, height: H, deviceScaleFactor: SCALE, mobile: false });
|
||||
await send('Runtime.enable');
|
||||
|
||||
const js = async (code) => {
|
||||
const r = await send('Runtime.evaluate', { expression: `(async () => { ${code} })()`, awaitPromise: true, returnByValue: true });
|
||||
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text);
|
||||
return r.result.value;
|
||||
};
|
||||
const key = async (name, modifiers = []) => {
|
||||
const m = modifiers.reduce((a, k) => a | MODS[k], 0);
|
||||
const [k, vk] = KEYS[name] || [name, name.length === 1 ? name.toUpperCase().charCodeAt(0) : 0];
|
||||
const text = !m || m === 8 ? (name.length === 1 ? name : undefined) : undefined;
|
||||
const code = name.length === 1 && /[a-z]/i.test(name) ? `Key${name.toUpperCase()}` : /^\d$/.test(name) ? `Digit${name}` : k;
|
||||
await send('Input.dispatchKeyEvent', { type: 'rawKeyDown', key: k, code, windowsVirtualKeyCode: vk, modifiers: m });
|
||||
await send('Input.dispatchKeyEvent', { type: 'keyUp', key: k, code, windowsVirtualKeyCode: vk, modifiers: m });
|
||||
await sleep(250);
|
||||
};
|
||||
const type = async (t, d = 45) => {
|
||||
for (const ch of t) {
|
||||
await send('Input.dispatchKeyEvent', { type: 'keyDown', key: ch, text: ch, unmodifiedText: ch, windowsVirtualKeyCode: ch.toUpperCase().charCodeAt(0) });
|
||||
await send('Input.dispatchKeyEvent', { type: 'keyUp', key: ch });
|
||||
await sleep(d);
|
||||
}
|
||||
await sleep(200);
|
||||
};
|
||||
const click = async (x, y) => {
|
||||
await send('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y });
|
||||
await send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
|
||||
await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
|
||||
await sleep(250);
|
||||
};
|
||||
const shot = async (name) => {
|
||||
await sleep(500);
|
||||
const img = await wc.capturePage();
|
||||
const file = path.join(OUT, `${name}-${theme}.png`);
|
||||
const r = await send('Page.captureScreenshot', { format: 'png' });
|
||||
const file = path.join(OUT, `${name}-${SUFFIX}.png`);
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
fs.writeFileSync(file, img.toPNG());
|
||||
const s = img.getSize();
|
||||
console.log(`saved ${path.basename(file)} ${s.width}x${s.height}`);
|
||||
fs.writeFileSync(file, Buffer.from(r.data, 'base64'));
|
||||
console.log(`saved ${path.basename(file)}`);
|
||||
};
|
||||
const ctx = { win, wc, js, key, type, click, shot, sleep, vault, W, H };
|
||||
const ctx = { js, key, type, click, shot, sleep, vault, W, H, send };
|
||||
|
||||
try {
|
||||
await sleep(2200);
|
||||
await sleep(3000);
|
||||
for (const [name, fn] of Object.entries(scenes)) {
|
||||
if (ONLY && !ONLY.includes(name)) continue;
|
||||
try { await fn(ctx); } catch (e) { console.log(`FAILED ${name}: ${e.message}`); }
|
||||
}
|
||||
} finally {
|
||||
app.exit(0);
|
||||
ws.close();
|
||||
proc.kill();
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -1,12 +1,14 @@
|
||||
#!/usr/bin/env sh
|
||||
# Captures all website screenshots from the real Ink app (read only) into src/assets/screens/.
|
||||
# Usage: scripts/capture/run.sh (env: INK_APP, OUT, SCENES)
|
||||
# Captures all website screenshots from the released Ink AppImage into src/assets/screens/.
|
||||
# Usage: scripts/capture/run.sh Ink-1.0.0-beta-x86_64.AppImage (env: OUT, SCENES)
|
||||
set -e
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
INK_APP="${INK_APP:-$HERE/../../../../App/Ink}"
|
||||
INK_APP="$(cd "$INK_APP" && pwd)"
|
||||
export INK_APP
|
||||
APPIMAGE="$(realpath "${1:-${INK_APPIMAGE:?pass the AppImage path}}")"
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
# Extract in an empty directory of its own; the AppImage is only unpacked, never run from here.
|
||||
(cd "$WORK" && cp "$APPIMAGE" ink.AppImage && chmod +x ink.AppImage && ./ink.AppImage --appimage-extract >/dev/null)
|
||||
export INK_BIN="$WORK/squashfs-root/ink"
|
||||
for theme in dark light; do
|
||||
THEME=$theme "$INK_APP/node_modules/.bin/electron" "$HERE/capture.cjs" \
|
||||
--ozone-platform=headless --force-device-scale-factor=1.5 --no-sandbox 2>&1 | grep -E 'saved|FAILED|Error' || true
|
||||
THEME=$theme node "$HERE/capture.cjs" 2>&1 | grep -E 'saved|FAILED|Error' || true
|
||||
done
|
||||
@@ -110,7 +110,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
async settings(ctx) {
|
||||
const { sleep, shot, key } = ctx;
|
||||
const { js, sleep, shot, key } = ctx;
|
||||
await reset(ctx);
|
||||
await openFromSidebar(ctx, 'Roadmap');
|
||||
await key(',', ['control']);
|
||||
@@ -132,4 +132,39 @@ module.exports = {
|
||||
await shot('history');
|
||||
await key('Escape');
|
||||
},
|
||||
|
||||
async databases(ctx) {
|
||||
const { js, sleep, shot } = ctx;
|
||||
await reset(ctx);
|
||||
await openFromSidebar(ctx, 'Feature tracker');
|
||||
await sleep(1200);
|
||||
await scrollPage(js, 230);
|
||||
await sleep(400);
|
||||
await shot('databases');
|
||||
},
|
||||
|
||||
async 'database-board'(ctx) {
|
||||
const { js, sleep, shot } = ctx;
|
||||
await reset(ctx);
|
||||
await openFromSidebar(ctx, 'Feature tracker');
|
||||
await sleep(800);
|
||||
await js(`[...document.querySelectorAll('button, [role="tab"]')].find((b) => b.textContent.trim() === 'Board')?.click()`);
|
||||
await sleep(1000);
|
||||
await scrollPage(js, 230);
|
||||
await sleep(400);
|
||||
await shot('database-board');
|
||||
},
|
||||
|
||||
// The settings dialog on the color theme and accent pickers. Run once per palette by run.sh.
|
||||
async themes(ctx) {
|
||||
const { js, sleep, shot, key } = ctx;
|
||||
await reset(ctx);
|
||||
await openFromSidebar(ctx, 'Roadmap');
|
||||
await key(',', ['control']);
|
||||
await sleep(900);
|
||||
await js(`document.querySelector('[aria-label="Color theme"]')?.scrollIntoView({ block: 'center' })`);
|
||||
await sleep(500);
|
||||
await shot('themes');
|
||||
await key('Escape');
|
||||
},
|
||||
};
|
||||
@@ -60,7 +60,7 @@ function seedVault(root) {
|
||||
- [x] Ship the first public build as an AppImage
|
||||
- [x] Canvas export to PNG, SVG and PDF
|
||||
- [x] Version history for pages and canvases
|
||||
- [ ] Test the Windows installer on a clean machine
|
||||
- [x] Windows installer and Linux deb
|
||||
- [ ] Write the docs, starting with [[Keyboard shortcuts]]
|
||||
- [ ] Follow the [[Launch plan]]
|
||||
|
||||
@@ -69,8 +69,8 @@ function seedVault(root) {
|
||||
|
||||
\`\`\`js
|
||||
const release = {
|
||||
version: '0.1.0',
|
||||
platforms: ['linux'],
|
||||
version: '1.0.0-beta',
|
||||
platforms: ['linux', 'windows'],
|
||||
notes: 'plain .md files, offline, no account',
|
||||
};
|
||||
console.log(\`Shipping Ink \${release.version}\`);
|
||||
@@ -79,7 +79,7 @@ console.log(\`Shipping Ink \${release.version}\`);
|
||||
<details>
|
||||
<summary>Why Linux first?</summary>
|
||||
|
||||
The AppImage is the only target built and tested so far. Windows and macOS are configured but still need a proper pass.
|
||||
The AppImage was the first target built and tested. Windows and the deb followed; macOS is still to do.
|
||||
|
||||
</details>
|
||||
`);
|
||||
@@ -142,7 +142,6 @@ tags: [work, ink]
|
||||
|
||||
## To do
|
||||
|
||||
- [ ] Windows installer test pass
|
||||
- [ ] macOS dmg build
|
||||
- [ ] Docs: where notes are stored
|
||||
- [ ] Verify checksum instructions
|
||||
@@ -151,13 +150,14 @@ tags: [work, ink]
|
||||
## Doing
|
||||
|
||||
- [ ] Website screenshots
|
||||
- [ ] Changelog for 0.1.0
|
||||
- [ ] Changelog for 1.0.0-beta
|
||||
- [ ] Spellcheck language picker polish
|
||||
|
||||
|
||||
## Done
|
||||
|
||||
- [x] AppImage build
|
||||
- [x] AppImage and deb builds
|
||||
- [x] Windows installer
|
||||
- [x] Canvas export (PNG, SVG, PDF)
|
||||
- [x] Version history for canvases
|
||||
- [x] Graph view filters
|
||||
@@ -199,6 +199,42 @@ The release is a single AppImage, so the checklist is short: bump the version, w
|
||||
Once that is done, the website can go live with a plain download button and nothing else.
|
||||
`);
|
||||
|
||||
|
||||
// A database: the page holds the schema, every record is a sub-page whose properties are front matter.
|
||||
const dbSchema = {
|
||||
properties: [
|
||||
{ key: 'status', name: 'Status', type: 'select', options: ['Backlog', 'In progress', 'Review', 'Done'], colors: { Backlog: 'gray', 'In progress': 'blue', Review: 'yellow', Done: 'green' } },
|
||||
{ key: 'area', name: 'Area', type: 'select', options: ['Editor', 'Canvas', 'Databases', 'Themes', 'Site'], colors: { Editor: 'pink', Canvas: 'purple', Databases: 'teal', Themes: 'orange', Site: 'blue' } },
|
||||
{ key: 'owner', name: 'Owner', type: 'people' },
|
||||
{ key: 'start', name: 'Start', type: 'date' },
|
||||
{ key: 'due', name: 'Due', type: 'date' },
|
||||
{ key: 'progress', name: 'Progress', type: 'number', format: 'bar', max: 100 },
|
||||
{ key: 'shipped', name: 'Shipped', type: 'checkbox' },
|
||||
],
|
||||
views: [
|
||||
{ id: 'vtable', name: 'Table', type: 'table', sort: { key: 'due', dir: 'asc' }, filters: [], hidden: [], widths: {}, calc: {} },
|
||||
{ id: 'vboard', name: 'Board', type: 'board', groupBy: 'status', sort: null, filters: [], hidden: [], widths: {}, calc: {} },
|
||||
{ id: 'vcal', name: 'Calendar', type: 'calendar', dateProp: 'due', sort: null, filters: [], hidden: [], widths: {}, calc: {} },
|
||||
{ id: 'vtime', name: 'Timeline', type: 'timeline', dateProp: 'start', endProp: 'due', sort: null, filters: [], hidden: [], widths: {}, calc: {} },
|
||||
],
|
||||
};
|
||||
w(root, 'Feature tracker.md', fm({ icon: '🧭', tags: ['work', 'ink'], 'ink-database': 'basic' }) + `Everything planned for the next release, one record per feature.\n\n%% ink-database\n${JSON.stringify(dbSchema, null, 2)}\n%%\n`);
|
||||
const recs = [
|
||||
['Color themes', 'Themes', 'Done', ['Mara'], '2026-09-14', '2026-09-26', 100, true],
|
||||
['Accent colors', 'Themes', 'Done', ['Mara'], '2026-09-20', '2026-10-01', 100, true],
|
||||
['Table view', 'Databases', 'Done', ['Jo'], '2026-09-08', '2026-10-02', 100, true],
|
||||
['Board view', 'Databases', 'Review', ['Jo', 'Ana'], '2026-09-24', '2026-10-12', 80, false],
|
||||
['Calendar view', 'Databases', 'In progress', ['Ana'], '2026-10-01', '2026-10-16', 60, false],
|
||||
['Timeline view', 'Databases', 'In progress', ['Jo'], '2026-10-05', '2026-10-22', 40, false],
|
||||
['Formula properties', 'Databases', 'Backlog', ['Jo'], '2026-10-14', '2026-10-30', 10, false],
|
||||
['Canvas alignment guides', 'Canvas', 'Done', ['Ana'], '2026-09-01', '2026-09-18', 100, true],
|
||||
['Quick capture', 'Editor', 'Review', ['Mara'], '2026-10-03', '2026-10-14', 90, false],
|
||||
['Website refresh', 'Site', 'In progress', ['Mara', 'Jo'], '2026-10-08', '2026-10-20', 55, false],
|
||||
];
|
||||
for (const [t, area, status, owner, start, due, progress, shipped] of recs) {
|
||||
w(root, `Feature tracker/${t}.md`, fm({ area, status, owner, start, due, progress, shipped }) + `${t}: notes and decisions live here, as an ordinary page.\n`);
|
||||
}
|
||||
|
||||
// Earlier versions of the Roadmap page, so version history has something to show.
|
||||
const now = Date.now();
|
||||
const hist = [
|
||||
@@ -217,7 +253,7 @@ Once that is done, the website can go live with a plain download button and noth
|
||||
return {
|
||||
lastPage: 'Roadmap',
|
||||
vaultSettings: {
|
||||
pins: ['Roadmap', 'Reading list', 'Sprint board', 'Project map.canvas'],
|
||||
pins: ['Roadmap', 'Feature tracker', 'Sprint board', 'Project map.canvas'],
|
||||
recent: ['Roadmap', 'Reading list', 'Sprint board'],
|
||||
tagColors: { work: 'blue', ink: 'pink', reading: 'yellow', learning: 'teal', personal: 'green', ideas: 'purple' },
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user