feat: the OS can hand a document to the installed app
The manifest registers .odt/.ott/.docx/.dotx as file handlers and a launchQueue consumer opens what the OS passes, keeping the handle so the first Save writes that file. A .dotx now reads like an .ott: Word's importer, but never bound as the file to save over.
This commit is contained in:
1 parent
d0d2ea2338
commit
e1b2fefa55
4 files changed
+40
-9
No files matched your search
+1
-1
@@ -134,6 +134,7 @@
|
||||
- Captions (References ▸ Caption, LibreOffice's Insert ▸ Caption): a caption paragraph in the `Caption` style with a running number that counts itself — one counter per category (figure / table), in document order, renumbering as the document is edited. Round-trips as an ODF `<text:sequence>` (with LibreOffice's own `ooow:Illustration+1` formula) and a Word `SEQ Figure \* ARABIC` field
|
||||
- Word (.docx) export and import — round-trips the editor's formatting (text, fonts, lists, tables, images, headers/footers, page geometry) and opens real Word documents
|
||||
- Document properties (title, subject, author, keywords, comments) in the File menu — LibreOffice's File ▸ Properties / Word's File ▸ Info. Round-trips through ODF `meta.xml` (one `meta:keyword` per keyword) and DOCX `docProps/core.xml`
|
||||
- File handlers: the installed app registers for `.odt` / `.ott` / `.docx` / `.dotx`, so the OS offers it for a double-clicked document and hands the file over through `launchQueue` — with its handle, so the first Save writes that same file. Word templates (`.dotx`) open like `.ott` does: read for their content, never bound as the file to save over
|
||||
- Installable and offline (`public/manifest.webmanifest`, `public/sw.js`): nothing here talks to a server at runtime, so once the app's files are cached it is the whole working editor with the network gone — spell checker, fonts and all. The service worker reads the build's own hashed asset names out of the entry document, which precaches the shell with no build step; what the app loads lazily (a dictionary, the speller's WASM) is kept as it is asked for, so it joins from the second visit on. The document itself is network-first, or a cached one would keep naming the assets of the build it was cached with. Verified in a headless Chromium with the network cut: the app reloads, styles and all, and the manifest validates clean
|
||||
- PDF export — Raster (pixel-exact copy of the editor with a selectable text layer) and Vector (crisp, fonts embedded, via the browser print dialog)
|
||||
- Print (printer button / Ctrl+P) — opens the browser print dialog with a pixel-exact raster of the document (tables, headers/footers and page breaks intact)
|
||||
@@ -162,7 +163,6 @@ The gap against Word/LibreOffice, most valuable first. Reviewed 2026-08-14.
|
||||
- Vertical writing modes (ODF `tb-rl`) and Asian typography (ruby)
|
||||
- Password-protected ODT/DOCX; digital signatures
|
||||
- Split view: two views of one document, scrolled independently
|
||||
- File handlers, so the OS can offer the installed app for a double-clicked `.odt`/`.docx` (Chrome's `file_handlers` + `launchQueue`)
|
||||
- A committed corpus of documents we author ourselves, so CI can test against real files
|
||||
(`.gitignore` excludes all of `tests/render-parity/fixtures/` today)
|
||||
|
||||
|
||||
@@ -15,5 +15,16 @@
|
||||
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" },
|
||||
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
||||
],
|
||||
"file_handlers": [
|
||||
{
|
||||
"action": "/",
|
||||
"accept": {
|
||||
"application/vnd.oasis.opendocument.text": [".odt"],
|
||||
"application/vnd.oasis.opendocument.text-template": [".ott"],
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"],
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.template": [".dotx"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+26
-6
@@ -64,6 +64,11 @@
|
||||
import { registerEmbeddedFonts, clearEmbeddedFonts } from './lib/fonts/embeddedFonts';
|
||||
import { saveEmbeddedFonts, loadEmbeddedFonts, clearEmbeddedFontStore } from './lib/storage/embeddedFontStore';
|
||||
|
||||
// launchQueue is not in lib.dom yet; reach it through this shape.
|
||||
type WithLaunchQueue = Window & {
|
||||
launchQueue?: { setConsumer: (c: (p: { files: FileSystemFileHandle[] }) => void) => void };
|
||||
};
|
||||
|
||||
let editor: Editor | null = $state(null);
|
||||
let tick: number = $state(0);
|
||||
let currentPage: number = $state(1);
|
||||
@@ -552,10 +557,11 @@
|
||||
async function applyImport(bytes: Uint8Array, handle: FileSystemFileHandle | null, sourceName?: string) {
|
||||
if (!editor) return;
|
||||
try {
|
||||
const isDocx = sourceName?.toLowerCase().endsWith('.docx');
|
||||
// An .ott is a template: load its content but don't bind the handle, so the
|
||||
// first Save prompts for a new .odt instead of overwriting the template.
|
||||
const isTemplate = sourceName?.toLowerCase().endsWith('.ott');
|
||||
const name = sourceName?.toLowerCase() ?? '';
|
||||
const isDocx = name.endsWith('.docx') || name.endsWith('.dotx');
|
||||
// A template is loaded for its content but never bound as the handle, so the
|
||||
// first Save prompts for a new document instead of overwriting the template.
|
||||
const isTemplate = name.endsWith('.ott') || name.endsWith('.dotx');
|
||||
// Pre-decode any images in a format the browser can't render (TIFF, …) to PNG.
|
||||
// Lazy: the decoder loads only when such an image is present, else this is a no-op.
|
||||
const converted = await convertUnsupportedImages(bytes);
|
||||
@@ -575,7 +581,7 @@
|
||||
documentEpoch++;
|
||||
resetHistory();
|
||||
// Adopt the opened file's name as the document name (drives the save filename).
|
||||
if (sourceName) documentName = stripOdtExtension(sourceName).replace(/\.docx$/i, '');
|
||||
if (sourceName) documentName = stripOdtExtension(sourceName).replace(/\.do[ct]x$/i, '');
|
||||
// Adopt the document's page geometry; the $effects persist it and
|
||||
// Editor.svelte re-paginates.
|
||||
if (result.margins) pageMargins = result.margins;
|
||||
@@ -656,6 +662,20 @@
|
||||
return () => window.removeEventListener(OPEN_COMMENT_EVENT, open);
|
||||
});
|
||||
|
||||
// A document double-clicked in the OS reaches the installed app through launchQueue
|
||||
// (the manifest's `file_handlers`). It fires once at startup, so it waits for the
|
||||
// editor; the handle comes with write permission, so a later Save writes that file.
|
||||
$effect(() => {
|
||||
const queue = (window as WithLaunchQueue).launchQueue;
|
||||
if (!editor || !queue) return;
|
||||
queue.setConsumer(async ({ files }) => {
|
||||
const handle = files?.[0];
|
||||
if (!handle) return;
|
||||
const file = await handle.getFile();
|
||||
await applyImport(new Uint8Array(await file.arrayBuffer()), handle, file.name);
|
||||
});
|
||||
});
|
||||
|
||||
async function handleOpen() {
|
||||
if (!editor) return;
|
||||
if (fsSupported) {
|
||||
@@ -1287,7 +1307,7 @@
|
||||
<input
|
||||
bind:this={fileInput}
|
||||
type="file"
|
||||
accept=".odt,.ott,.docx,application/vnd.oasis.opendocument.text,application/vnd.oasis.opendocument.text-template,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
accept=".odt,.ott,.docx,.dotx,application/vnd.oasis.opendocument.text,application/vnd.oasis.opendocument.text-template,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.wordprocessingml.template"
|
||||
class="file-input"
|
||||
onchange={handleImportFile}
|
||||
/>
|
||||
|
||||
@@ -21,10 +21,10 @@ const TEMPLATE_PICKER_TYPES = [
|
||||
{ description: 'Word Template', accept: { [DOTX_MIME]: ['.dotx'] } },
|
||||
];
|
||||
|
||||
// The open picker also accepts .ott templates (read-only; saving stays .odt-only).
|
||||
// The open picker also accepts templates (read-only; saving stays .odt-only).
|
||||
const OPEN_PICKER_TYPES = [
|
||||
{ description: 'OpenDocument Text', accept: { [ODT_MIME]: ['.odt'], [OTT_MIME]: ['.ott'] } },
|
||||
...DOCX_PICKER_TYPES,
|
||||
{ description: 'Word Document', accept: { [DOCX_MIME]: ['.docx'], [DOTX_MIME]: ['.dotx'] } },
|
||||
];
|
||||
|
||||
// showSaveFilePicker/showOpenFilePicker are not in lib.dom yet; reach them via casts.
|
||||
|
||||
Reference in new issue
Block a user