bookish-press 0.9.4 → 0.10.0
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/dist/components/Auth.svelte +37 -21
- package/dist/components/BookPreview.svelte +4 -4
- package/dist/components/BookPreview.svelte.d.ts +1 -1
- package/dist/components/Link.svelte +19 -12
- package/dist/components/app/Button.svelte +1 -8
- package/dist/components/app/Link.svelte +1 -7
- package/dist/components/app/Options.svelte +1 -6
- package/dist/components/app/TextInput.svelte +2 -8
- package/dist/components/chapter/Atom.svelte +2 -1
- package/dist/components/chapter/Atom.svelte.d.ts +1 -1
- package/dist/components/chapter/Block.svelte +9 -9
- package/dist/components/chapter/Block.svelte.d.ts +1 -1
- package/dist/components/chapter/Callout.svelte.d.ts +1 -1
- package/dist/components/chapter/CaptionedCode.svelte +1 -0
- package/dist/components/chapter/CaptionedCode.svelte.d.ts +1 -1
- package/dist/components/chapter/ChapterBody.svelte +5 -2
- package/dist/components/chapter/ChapterBody.svelte.d.ts +1 -1
- package/dist/components/chapter/ChapterBody.test.js +3 -3
- package/dist/components/chapter/Citations.svelte +17 -16
- package/dist/components/chapter/Citations.svelte.d.ts +1 -1
- package/dist/components/chapter/Code.svelte +8 -14
- package/dist/components/chapter/Comment.svelte +7 -12
- package/dist/components/chapter/Comment.svelte.d.ts +1 -1
- package/dist/components/chapter/Definition.svelte +8 -9
- package/dist/components/chapter/Definition.svelte.d.ts +1 -1
- package/dist/components/chapter/Embed.svelte +41 -61
- package/dist/components/chapter/Embed.svelte.d.ts +1 -1
- package/dist/components/chapter/ErrorMessage.svelte.d.ts +1 -1
- package/dist/components/chapter/Figure.svelte +14 -14
- package/dist/components/chapter/Figure.svelte.d.ts +4 -4
- package/dist/components/chapter/Footnote.svelte +11 -17
- package/dist/components/chapter/Footnote.svelte.d.ts +1 -1
- package/dist/components/chapter/Format.svelte +11 -11
- package/dist/components/chapter/Format.svelte.d.ts +1 -1
- package/dist/components/chapter/InlineCode.svelte.d.ts +1 -1
- package/dist/components/chapter/Label.svelte +1 -1
- package/dist/components/chapter/Label.svelte.d.ts +1 -1
- package/dist/components/chapter/LineBreak.svelte +1 -0
- package/dist/components/chapter/LineBreak.svelte.d.ts +1 -1
- package/dist/components/chapter/Link.svelte +14 -12
- package/dist/components/chapter/Link.svelte.d.ts +1 -1
- package/dist/components/chapter/List.svelte +1 -1
- package/dist/components/chapter/List.svelte.d.ts +1 -1
- package/dist/components/chapter/Marginal.svelte +36 -42
- package/dist/components/chapter/Paragraph.svelte +4 -4
- package/dist/components/chapter/Paragraph.svelte.d.ts +1 -1
- package/dist/components/chapter/Positioned.svelte +1 -0
- package/dist/components/chapter/Positioned.svelte.d.ts +1 -1
- package/dist/components/chapter/Python.svelte +42 -31
- package/dist/components/chapter/Python.svelte.d.ts +1 -1
- package/dist/components/chapter/Quote.svelte.d.ts +1 -1
- package/dist/components/chapter/Rule.svelte +1 -0
- package/dist/components/chapter/Rule.svelte.d.ts +1 -1
- package/dist/components/chapter/Table.svelte.d.ts +1 -1
- package/dist/components/chapter/Text.svelte +41 -50
- package/dist/components/chapter/Text.svelte.d.ts +1 -1
- package/dist/components/controls/DarkToggle.svelte +4 -4
- package/dist/components/editor/BookishEditor.svelte +605 -569
- package/dist/components/editor/BookishEditor.svelte.d.ts +1 -1
- package/dist/components/editor/CalloutEditor.svelte +1 -1
- package/dist/components/editor/CalloutEditor.svelte.d.ts +1 -1
- package/dist/components/editor/CaptionedCodeEditor.svelte +1 -1
- package/dist/components/editor/CaptionedCodeEditor.svelte.d.ts +1 -1
- package/dist/components/editor/CaretContext.d.ts +14 -14
- package/dist/components/editor/CaretState.d.ts +3 -3
- package/dist/components/editor/CaretView.svelte +8 -19
- package/dist/components/editor/CitationsEditor.svelte +10 -18
- package/dist/components/editor/CitationsEditor.svelte.d.ts +1 -1
- package/dist/components/editor/Clipboard.d.ts +1 -1
- package/dist/components/editor/Command.d.ts +1 -1
- package/dist/components/editor/Commands.js +19 -19
- package/dist/components/editor/ConfirmButton.svelte +1 -6
- package/dist/components/editor/DefinitionEditor.svelte +9 -7
- package/dist/components/editor/DefinitionEditor.svelte.d.ts +1 -1
- package/dist/components/editor/EmbedEditor.svelte +25 -34
- package/dist/components/editor/EmbedEditor.svelte.d.ts +1 -1
- package/dist/components/editor/ImageChooser.svelte +8 -7
- package/dist/components/editor/ImageChooser.svelte.d.ts +1 -1
- package/dist/components/editor/ImageThumbnail.svelte +21 -18
- package/dist/components/editor/InlineCodeEditor.svelte +1 -1
- package/dist/components/editor/InlineCodeEditor.svelte.d.ts +1 -1
- package/dist/components/editor/LabelEditor.svelte +2 -2
- package/dist/components/editor/LabelEditor.svelte.d.ts +1 -1
- package/dist/components/editor/LanguageEditor.svelte +8 -1
- package/dist/components/editor/LinkEditor.svelte +42 -44
- package/dist/components/editor/LinkEditor.svelte.d.ts +1 -1
- package/dist/components/editor/Permissions.svelte +23 -29
- package/dist/components/editor/PositionEditor.svelte +1 -1
- package/dist/components/editor/PositionEditor.svelte.d.ts +1 -1
- package/dist/components/editor/QuoteEditor.svelte +1 -1
- package/dist/components/editor/QuoteEditor.svelte.d.ts +1 -1
- package/dist/components/editor/Switch.svelte +5 -11
- package/dist/components/editor/TableEditor.svelte +1 -1
- package/dist/components/editor/TableEditor.svelte.d.ts +1 -1
- package/dist/components/editor/TextEditor.svelte +61 -58
- package/dist/components/editor/Toggle.svelte +16 -14
- package/dist/components/editor/Toolbar.svelte +81 -94
- package/dist/components/editor/UndoState.d.ts +2 -2
- package/dist/components/page/Acknowledgements.svelte +2 -8
- package/dist/components/page/Authors.svelte +12 -21
- package/dist/components/page/BookLoader.svelte +32 -34
- package/dist/components/page/BulkReferenceEditor.svelte +21 -41
- package/dist/components/page/Chapter.svelte +139 -114
- package/dist/components/page/Chapter.svelte.d.ts +1 -1
- package/dist/components/page/ChapterTitle.svelte +1 -1
- package/dist/components/page/Contexts.js +3 -1
- package/dist/components/page/DefinitionView.svelte +30 -42
- package/dist/components/page/DefinitionView.svelte.d.ts +1 -1
- package/dist/components/page/Description.svelte +2 -8
- package/dist/components/page/DownloadEPUB.svelte +83 -41
- package/dist/components/page/EditableHeader.svelte.d.ts +1 -1
- package/dist/components/page/Edition.svelte +44 -30
- package/dist/components/page/Edition.svelte.d.ts +1 -1
- package/dist/components/page/EditionLoader.svelte +101 -100
- package/dist/components/page/Editions.svelte +13 -13
- package/dist/components/page/Glossary.svelte +9 -10
- package/dist/components/page/Header.svelte +15 -28
- package/dist/components/page/Index.svelte +17 -18
- package/dist/components/page/License.svelte +2 -8
- package/dist/components/page/Media.svelte +7 -14
- package/dist/components/page/MediaPreview.svelte +14 -17
- package/dist/components/page/Outline.svelte +74 -66
- package/dist/components/page/Page.svelte +23 -17
- package/dist/components/page/PossibleChapter.svelte +2 -2
- package/dist/components/page/PossibleReference.svelte +1 -1
- package/dist/components/page/PossibleReference.svelte.d.ts +2 -2
- package/dist/components/page/Print.svelte +5 -8
- package/dist/components/page/Publish.svelte +33 -39
- package/dist/components/page/Reference.svelte +9 -11
- package/dist/components/page/Reference.svelte.d.ts +1 -1
- package/dist/components/page/References.svelte +1 -1
- package/dist/components/page/Search.svelte +39 -40
- package/dist/components/page/SubdomainEditor.svelte +11 -10
- package/dist/components/page/TableOfContents.svelte +52 -48
- package/dist/components/page/TableOfContentsImage.svelte +2 -2
- package/dist/components/page/TableOfContentsRow.svelte +9 -22
- package/dist/components/page/TableOfContentsRow.svelte.d.ts +2 -2
- package/dist/components/page/ThemeEditor.svelte +10 -17
- package/dist/components/page/ThemeEditorPreview.svelte +3 -6
- package/dist/components/page/ThemeEditorPreview.svelte.d.ts +1 -1
- package/dist/components/page/ThemeVariableEditor.svelte +6 -6
- package/dist/components/page/Unknown.svelte +1 -1
- package/dist/epub/build.d.ts +6 -0
- package/dist/epub/build.js +16 -3
- package/dist/epub/build.test.js +65 -0
- package/dist/epub/images.d.ts +7 -5
- package/dist/epub/images.js +15 -10
- package/dist/epub/images.test.js +8 -6
- package/dist/epub/package.js +25 -3
- package/dist/models/FirebaseAvailable.d.ts +9 -0
- package/dist/models/FirebaseAvailable.js +10 -0
- package/dist/models/book/BookMedia.d.ts +2 -1
- package/dist/models/book/BookMedia.js +33 -9
- package/package.json +18 -14
|
@@ -1,59 +1,80 @@
|
|
|
1
1
|
<script lang="ts">import { onDestroy } from "svelte";
|
|
2
2
|
import { getBase, getEdition } from "./Contexts";
|
|
3
|
-
import { buildEPUB } from "../../epub/build";
|
|
4
|
-
import { DEFAULT_SIZE, SIZES } from "../../epub/images";
|
|
3
|
+
import { buildEPUB } from "../../epub/build.js";
|
|
4
|
+
import { DEFAULT_SIZE, SIZES } from "../../epub/images.js";
|
|
5
5
|
let edition = getEdition();
|
|
6
6
|
let base = getBase();
|
|
7
7
|
let sizeID = $state(DEFAULT_SIZE.id);
|
|
8
|
+
let grayscale = $state(false);
|
|
8
9
|
let building = $state(false);
|
|
9
|
-
let progress = $state(
|
|
10
|
-
let error = $state(
|
|
10
|
+
let progress = $state(undefined);
|
|
11
|
+
let error = $state(undefined);
|
|
11
12
|
let warnings = $state([]);
|
|
12
|
-
let url = $state(
|
|
13
|
+
let url = $state(undefined);
|
|
13
14
|
let filename = $state("book.epub");
|
|
15
|
+
/** Release the previous blob before replacing or discarding it. */
|
|
14
16
|
function release() {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
if (url !== undefined) {
|
|
18
|
+
URL.revokeObjectURL(url);
|
|
19
|
+
url = undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Any change to the controls makes an already-built file stale: without
|
|
24
|
+
* this the reader could flip a setting and download something that doesn't
|
|
25
|
+
* match what the controls say.
|
|
26
|
+
*/
|
|
27
|
+
function invalidate() {
|
|
28
|
+
release();
|
|
29
|
+
warnings = [];
|
|
30
|
+
error = undefined;
|
|
19
31
|
}
|
|
20
32
|
function chooseSize(id) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
sizeID = id;
|
|
34
|
+
invalidate();
|
|
35
|
+
}
|
|
36
|
+
function chooseGrayscale(on) {
|
|
37
|
+
grayscale = on;
|
|
38
|
+
invalidate();
|
|
25
39
|
}
|
|
26
40
|
async function build() {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
if ($edition === undefined || building) return;
|
|
42
|
+
building = true;
|
|
43
|
+
release();
|
|
44
|
+
error = undefined;
|
|
45
|
+
warnings = [];
|
|
46
|
+
progress = {
|
|
47
|
+
phase: "images",
|
|
48
|
+
done: 0,
|
|
49
|
+
total: 0
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
// Everything below runs only on a click, so the browser-only APIs
|
|
53
|
+
// it reaches are never touched while server rendering.
|
|
54
|
+
const result = await buildEPUB($edition, {
|
|
55
|
+
base: $base ?? "",
|
|
56
|
+
size: SIZES[sizeID] ?? DEFAULT_SIZE,
|
|
57
|
+
grayscale,
|
|
58
|
+
onProgress: (update) => progress = update
|
|
59
|
+
});
|
|
60
|
+
url = URL.createObjectURL(result.blob);
|
|
61
|
+
filename = result.filename;
|
|
62
|
+
warnings = result.warnings;
|
|
63
|
+
} catch (problem) {
|
|
64
|
+
console.error(problem);
|
|
65
|
+
error = "Something went wrong building the e-book.";
|
|
66
|
+
} finally {
|
|
67
|
+
building = false;
|
|
68
|
+
progress = undefined;
|
|
69
|
+
}
|
|
49
70
|
}
|
|
71
|
+
// $derived.by rather than $derived: svelte2tsx mis-narrows the type of a
|
|
72
|
+
// value read inside a multiline $derived expression.
|
|
50
73
|
let status = $derived.by(() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return `Preparing chapter ${progress.done} of ${progress.total}\u2026`;
|
|
56
|
-
return "Packaging the book\u2026";
|
|
74
|
+
if (progress === undefined) return undefined;
|
|
75
|
+
if (progress.phase === "images") return progress.total === 0 ? "Gathering images…" : `Shrinking image ${progress.done} of ${progress.total}…`;
|
|
76
|
+
if (progress.phase === "chapters") return `Preparing chapter ${progress.done} of ${progress.total}…`;
|
|
77
|
+
return "Packaging the book…";
|
|
57
78
|
});
|
|
58
79
|
onDestroy(release);
|
|
59
80
|
</script>
|
|
@@ -76,6 +97,19 @@ onDestroy(release);
|
|
|
76
97
|
</select>
|
|
77
98
|
</span>
|
|
78
99
|
</label>
|
|
100
|
+
<label class="greyscale">
|
|
101
|
+
<input
|
|
102
|
+
type="checkbox"
|
|
103
|
+
checked={grayscale}
|
|
104
|
+
disabled={building}
|
|
105
|
+
onchange={(event) =>
|
|
106
|
+
chooseGrayscale(event.currentTarget.checked)}
|
|
107
|
+
/>
|
|
108
|
+
Greyscale
|
|
109
|
+
<span class="note"
|
|
110
|
+
>(a little smaller; e-readers convert anyway)</span
|
|
111
|
+
>
|
|
112
|
+
</label>
|
|
79
113
|
<!-- A plain button rather than the app's, whose --app-* variables are
|
|
80
114
|
only defined in the (app) layout, so it renders unstyled here and
|
|
81
115
|
in books compiled by bookish-reader. -->
|
|
@@ -226,6 +260,14 @@ onDestroy(release);
|
|
|
226
260
|
text-decoration: underline;
|
|
227
261
|
}
|
|
228
262
|
|
|
263
|
+
.greyscale {
|
|
264
|
+
gap: 0.35em;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.greyscale .note {
|
|
268
|
+
color: var(--bookish-muted-color);
|
|
269
|
+
}
|
|
270
|
+
|
|
229
271
|
.warnings {
|
|
230
272
|
font-size: var(--bookish-small-font-size);
|
|
231
273
|
font-style: italic;
|
|
@@ -2,47 +2,61 @@
|
|
|
2
2
|
import { goto } from "$app/navigation";
|
|
3
3
|
import { onMount, setContext, untrack } from "svelte";
|
|
4
4
|
import { writable } from "svelte/store";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
DARK_MODE,
|
|
8
|
-
getBook
|
|
9
|
-
} from "./Contexts";
|
|
10
|
-
import { BookishTheme } from "../../models/book/Theme";
|
|
5
|
+
import { BASE, DARK_MODE, getBook } from "./Contexts";
|
|
6
|
+
import { BookishTheme } from "../../models/book/Theme.js";
|
|
11
7
|
import { isDark, setDark } from "../../util/dark";
|
|
12
8
|
import { page } from "$app/state";
|
|
13
9
|
import Title from "./Title.svelte";
|
|
14
10
|
import Page from "./Page.svelte";
|
|
15
|
-
import Parser from "../../models/chapter/Parser";
|
|
16
|
-
import EmbedNode from "../../models/chapter/EmbedNode";
|
|
11
|
+
import Parser from "../../models/chapter/Parser.js";
|
|
12
|
+
import EmbedNode from "../../models/chapter/EmbedNode.js";
|
|
13
|
+
// Poly fill smooth scrolling for Safari.
|
|
17
14
|
onMount(() => smoothscroll.polyfill());
|
|
18
15
|
let { edition, base = "", children } = $props();
|
|
16
|
+
/** The cover image's URL for social previews. The cover is stored as
|
|
17
|
+
* Bookish embed markup, so it must be parsed; emitting the raw markup
|
|
18
|
+
* would produce a nonsense og:image (and stray pages, when a static
|
|
19
|
+
* site generator crawls it as a link). */
|
|
19
20
|
let coverURL = $derived.by(() => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
const cover = edition?.getImage("cover");
|
|
22
|
+
if (!cover) return undefined;
|
|
23
|
+
const node = Parser.parseEmbed(edition, cover);
|
|
24
|
+
const url = node instanceof EmbedNode ? node.getURL() : undefined;
|
|
25
|
+
if (!url || url.length === 0) return undefined;
|
|
26
|
+
// Resolve local images the same way Embed.svelte does.
|
|
27
|
+
return url.startsWith("http") ? url : `${base}/images/${url}`;
|
|
26
28
|
});
|
|
27
29
|
let book = getBook();
|
|
30
|
+
// Expose the base path to descendants, keeping it up to date when it changes.
|
|
31
|
+
// Deliberately seeded with the initial base (untracked); the effect
|
|
32
|
+
// below keeps it in sync when the prop changes.
|
|
28
33
|
let baseStore = writable(untrack(() => base));
|
|
29
34
|
setContext(BASE, baseStore);
|
|
30
35
|
$effect.pre(() => {
|
|
31
|
-
|
|
36
|
+
baseStore.set(base);
|
|
32
37
|
});
|
|
38
|
+
// Default dark mode to whatever's stored in local storage, if anything.
|
|
39
|
+
// respect user choice on the website despite the system theme
|
|
33
40
|
let darkMode = writable(isDark());
|
|
41
|
+
// Expose dark mode to descendants
|
|
34
42
|
setContext(DARK_MODE, darkMode);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
// The CSS to set on the edition, kept up to date with the edition's theme.
|
|
44
|
+
let themeCSS = $derived(themeToCSS(edition?.getTheme() ?? BookishTheme));
|
|
45
|
+
// When dark mode changes, update the body's class list.
|
|
38
46
|
$effect.pre(() => {
|
|
39
|
-
|
|
47
|
+
setDark($darkMode);
|
|
40
48
|
});
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
// Redirect old hash routes by simply replacing their hash before routing.
|
|
50
|
+
// SvelteKit 3's goto rejects paths that match no route, where SvelteKit 2
|
|
51
|
+
// fell back to a full page load; keep that fallback.
|
|
52
|
+
if (typeof window !== "undefined" && window.location.hash.startsWith("#/")) {
|
|
53
|
+
const path = location.hash.replace("#", "");
|
|
54
|
+
goto(path).catch(() => window.location.href = path);
|
|
55
|
+
}
|
|
56
|
+
/** Given a theme, compute the CSS rules that apply the theme. */
|
|
43
57
|
function themeToCSS(theme) {
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
if (theme === null) theme = BookishTheme;
|
|
59
|
+
return `
|
|
46
60
|
${(theme.imports ?? []).map((url) => `@import url(${url});`).join("\n")}
|
|
47
61
|
.bookish {
|
|
48
62
|
${theme.light ? toRules(theme.light) : ""}
|
|
@@ -57,13 +71,13 @@ function themeToCSS(theme) {
|
|
|
57
71
|
`;
|
|
58
72
|
}
|
|
59
73
|
function toRules(set) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
return Object.keys(set).map((name) => {
|
|
75
|
+
const value = set[name];
|
|
76
|
+
if (value.length > 0) {
|
|
77
|
+
const cssVariable = "--bookish-" + name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" ").map((s) => s.toLowerCase()).join("-").replace(/([0-9])/g, "-$1-");
|
|
78
|
+
return `${cssVariable}: ${value};`;
|
|
79
|
+
}
|
|
80
|
+
}).join("\n ");
|
|
67
81
|
}
|
|
68
82
|
</script>
|
|
69
83
|
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
<script lang="ts">import Loading from "./Loading.svelte";
|
|
2
2
|
import Edition from "./Edition.svelte";
|
|
3
3
|
import Feedback from "../app/Feedback.svelte";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
getEdition,
|
|
7
|
-
getStatus,
|
|
8
|
-
getUser,
|
|
9
|
-
getChapterText
|
|
10
|
-
} from "./Contexts";
|
|
11
|
-
import BookSaveStatus from "../../models/book/BookSaveStatus";
|
|
4
|
+
import { getBook, getEdition, getStatus, getUser, getChapterText } from "./Contexts.js";
|
|
5
|
+
import BookSaveStatus from "../../models/book/BookSaveStatus.js";
|
|
12
6
|
import { get } from "svelte/store";
|
|
13
7
|
import { onDestroy, untrack } from "svelte";
|
|
14
8
|
import { page } from "$app/state";
|
|
@@ -19,117 +13,124 @@ let auth = getUser();
|
|
|
19
13
|
let book = getBook();
|
|
20
14
|
let edition = getEdition();
|
|
21
15
|
let status = getStatus();
|
|
16
|
+
/** Whether the edition store has been set by something other than this
|
|
17
|
+
* loader's own snapshot application (i.e., a local edit) since the last
|
|
18
|
+
* completed save. While true, incoming snapshots are by definition older
|
|
19
|
+
* than local state and applying them would lose the newer edits, so they
|
|
20
|
+
* are skipped; the pending save's own confirmation snapshot arrives
|
|
21
|
+
* after the save completes and is applied then. (Reader routes never
|
|
22
|
+
* edit, so this stays false there.) */
|
|
22
23
|
let locallyEdited = false;
|
|
23
24
|
let applyingSnapshot = false;
|
|
24
25
|
const localEditWatch = edition.subscribe(() => {
|
|
25
|
-
|
|
26
|
+
if (!applyingSnapshot) locallyEdited = true;
|
|
26
27
|
});
|
|
28
|
+
// When a save completes, local state and server state agree again.
|
|
27
29
|
const statusWatch = status?.subscribe((current) => {
|
|
28
|
-
|
|
30
|
+
if (current === BookSaveStatus.Saved) locallyEdited = false;
|
|
29
31
|
});
|
|
30
32
|
function skipStaleSnapshot() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
const current = status ? get(status) : undefined;
|
|
34
|
+
return locallyEdited && (current === BookSaveStatus.Changed || current === BookSaveStatus.Saving);
|
|
33
35
|
}
|
|
34
36
|
function applySnapshot(apply) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
applyingSnapshot = true;
|
|
38
|
+
try {
|
|
39
|
+
apply();
|
|
40
|
+
} finally {
|
|
41
|
+
applyingSnapshot = false;
|
|
42
|
+
}
|
|
41
43
|
}
|
|
42
|
-
|
|
43
|
-
let
|
|
44
|
-
let
|
|
45
|
-
let
|
|
44
|
+
/** Mapping from Firestore docuemnt IDs to chapter text */
|
|
45
|
+
let chapterText = new Map();
|
|
46
|
+
let editionUnsub = undefined;
|
|
47
|
+
let chaptersUnsub = undefined;
|
|
48
|
+
let error = $state(undefined);
|
|
46
49
|
function unsub() {
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
// Unsubscribe to the old edition document if we were listening to one.
|
|
51
|
+
if (editionUnsub) editionUnsub();
|
|
52
|
+
if (chaptersUnsub) chaptersUnsub();
|
|
49
53
|
}
|
|
50
54
|
function listen() {
|
|
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
|
-
}
|
|
55
|
+
unsub();
|
|
56
|
+
error = undefined;
|
|
57
|
+
// If we no longer have a book, we can't get an edition.
|
|
58
|
+
if ($book === undefined) {
|
|
59
|
+
edition.set(undefined);
|
|
60
|
+
error = "Unknown book.";
|
|
61
|
+
return;
|
|
62
|
+
} else {
|
|
63
|
+
currentEditionID = page.params.editionid;
|
|
64
|
+
const latestPublished = !write;
|
|
65
|
+
// Figure out which edition to load.
|
|
66
|
+
let editionID;
|
|
67
|
+
if (currentEditionID === undefined) {
|
|
68
|
+
if (latestPublished) {
|
|
69
|
+
editionID = $book.getLatestPublishedEditionID();
|
|
70
|
+
if (editionID === undefined) error = "There is no published edition of this book.";
|
|
71
|
+
} else {
|
|
72
|
+
editionID = $book.getLatestEditionID();
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
editionID = $book.getEditionNumberID(parseFloat(currentEditionID));
|
|
76
|
+
if (editionID === undefined) error = `There is no ${currentEditionID} edition of this book`;
|
|
77
|
+
}
|
|
78
|
+
// Listen to the doc for changes and listen to all of its chapters.
|
|
79
|
+
if (editionID !== undefined) {
|
|
80
|
+
editionUnsub = listenToEdition($book.getID(), editionID, (ed) => {
|
|
81
|
+
if (skipStaleSnapshot()) return;
|
|
82
|
+
if (ed) {
|
|
83
|
+
// Before setting the new edition, augmented it with the chapter text.
|
|
84
|
+
applySnapshot(() => edition.set(ed.withChapterText(chapterText)));
|
|
85
|
+
} else {
|
|
86
|
+
applySnapshot(() => edition.set(undefined));
|
|
87
|
+
error = "Unable to load edition";
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
chaptersUnsub = listenToChapters($book.getID(), editionID, (chapters) => {
|
|
91
|
+
// Cache the text of the chapters we received — except
|
|
92
|
+
// text we already have while local edits await a save,
|
|
93
|
+
// which may be a stale echo of what the user since
|
|
94
|
+
// revised. Text for a chapter not yet cached is load
|
|
95
|
+
// data, not an echo; it must always be kept, since
|
|
96
|
+
// nothing will re-deliver it. (The effect below keeps
|
|
97
|
+
// this cache current with local edits, so applying it
|
|
98
|
+
// to the edition never loses anything.)
|
|
99
|
+
const stale = skipStaleSnapshot();
|
|
100
|
+
for (const [ref, text] of chapters) if (!stale || !chapterText.has(ref.id)) chapterText.set(ref.id, text);
|
|
101
|
+
// Update the chapter text in the current edition.
|
|
102
|
+
if ($edition) {
|
|
103
|
+
applySnapshot(() => edition.set($edition.withChapterText(chapterText)));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
111
108
|
}
|
|
109
|
+
// Remember the chapter text whenever the edition changes, so we don't overwrite stale text.
|
|
110
|
+
// when other things change.
|
|
112
111
|
$effect.pre(() => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
112
|
+
if ($edition) {
|
|
113
|
+
for (const chapter of $edition.chapters) {
|
|
114
|
+
if (chapter.ref && chapter.text !== null) chapterText.set(chapter.ref.id, chapter.text);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
119
117
|
});
|
|
118
|
+
// Whenever the requested edition or the user changes, change the listener.
|
|
119
|
+
// currentEditionID is read untracked because listen() writes it; tracking
|
|
120
|
+
// it would make this effect invalidate itself.
|
|
120
121
|
$effect(() => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
});
|
|
122
|
+
const requestedEditionID = page.params.editionid;
|
|
123
|
+
const user = $auth?.user;
|
|
124
|
+
untrack(() => {
|
|
125
|
+
if ((currentEditionID === undefined || requestedEditionID !== currentEditionID) && user !== null && user !== undefined) listen();
|
|
126
|
+
});
|
|
127
127
|
});
|
|
128
|
+
// When unmounted, unset the stores — no longer viewing a book.
|
|
128
129
|
onDestroy(() => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
localEditWatch();
|
|
131
|
+
statusWatch?.();
|
|
132
|
+
edition.set(undefined);
|
|
133
|
+
unsub();
|
|
133
134
|
});
|
|
134
135
|
</script>
|
|
135
136
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
<script lang="ts">import
|
|
2
|
-
import Instructions from "./Instructions.svelte";
|
|
1
|
+
<script lang="ts">import Instructions from "./Instructions.svelte";
|
|
3
2
|
import Note from "../editor/Note.svelte";
|
|
4
3
|
import { getBook, getUser, isBookEditable } from "./Contexts";
|
|
5
4
|
import Button from "../app/Button.svelte";
|
|
@@ -13,23 +12,24 @@ let editable = isBookEditable();
|
|
|
13
12
|
let user = getUser();
|
|
14
13
|
let publisher = $state(false);
|
|
15
14
|
async function updateUserClaims() {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
if ($user === undefined || $user.user === null) return;
|
|
16
|
+
const token = await $user.user.getIdTokenResult();
|
|
17
|
+
publisher = "publisher" in token.claims && token.claims.publisher === true;
|
|
19
18
|
}
|
|
20
19
|
async function createEdition() {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
if ($book === undefined) return;
|
|
21
|
+
try {
|
|
22
|
+
const { createNewEdition } = await import("../../models/CRUD");
|
|
23
|
+
const revisedBook = await createNewEdition($book);
|
|
24
|
+
book.set(revisedBook);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error("Error creating new edition:", err);
|
|
27
|
+
}
|
|
28
28
|
}
|
|
29
29
|
let editions = $derived($book?.getEditions());
|
|
30
30
|
let latestPublishedID = $derived($book?.getLatestPublishedEditionID());
|
|
31
31
|
$effect(() => {
|
|
32
|
-
|
|
32
|
+
if ($user && $user.user) updateUserClaims();
|
|
33
33
|
});
|
|
34
34
|
</script>
|
|
35
35
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">import Header from "./Header.svelte";
|
|
2
2
|
import Outline from "./Outline.svelte";
|
|
3
3
|
import Page from "./Page.svelte";
|
|
4
|
-
import ChapterIDs from "../../models/book/ChapterID";
|
|
4
|
+
import ChapterIDs from "../../models/book/ChapterID.js";
|
|
5
5
|
import DefinitionView from "./DefinitionView.svelte";
|
|
6
6
|
import { getUser, getEdition, isEditionEditable } from "./Contexts";
|
|
7
7
|
import Instructions from "./Instructions.svelte";
|
|
@@ -10,16 +10,15 @@ let auth = getUser();
|
|
|
10
10
|
let edition = getEdition();
|
|
11
11
|
let editable = isEditionEditable();
|
|
12
12
|
let glossary = $derived($edition?.getGlossary() ?? {});
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
));
|
|
13
|
+
// Sort by canonical phrases
|
|
14
|
+
let keys = $derived($edition === undefined ? [] : glossary === undefined || Object.keys(glossary).length === 0 ? null : Object.keys(glossary).sort((a, b) => glossary[a].phrase.localeCompare(glossary[b].phrase)));
|
|
16
15
|
function addEmptyDefinition() {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
if ($edition === undefined) return;
|
|
17
|
+
// Generate an ID.
|
|
18
|
+
const letters = "abcdefghijklmnopqrstuvwxyz".split("");
|
|
19
|
+
let id = "";
|
|
20
|
+
while (id.length < 4 || $edition.hasDefinition(id)) id = id + letters[Math.round(Math.random() * 26)];
|
|
21
|
+
edition.set($edition.withDefinition(id, "", "", []));
|
|
23
22
|
}
|
|
24
23
|
</script>
|
|
25
24
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script lang="ts">import Parser from "../../models/chapter/Parser";
|
|
1
|
+
<script lang="ts">import Parser from "../../models/chapter/Parser.js";
|
|
2
2
|
import Embed from "../chapter/Embed.svelte";
|
|
3
3
|
import BookishEditor from "../editor/BookishEditor.svelte";
|
|
4
4
|
import Title from "./Title.svelte";
|
|
@@ -7,45 +7,32 @@ import TextEditor from "../editor/TextEditor.svelte";
|
|
|
7
7
|
import { getUser, getEdition, getLeasee, lease } from "./Contexts";
|
|
8
8
|
import Button from "../app/Button.svelte";
|
|
9
9
|
import ConfirmButton from "../editor/ConfirmButton.svelte";
|
|
10
|
-
let {
|
|
11
|
-
editable,
|
|
12
|
-
label,
|
|
13
|
-
header,
|
|
14
|
-
id,
|
|
15
|
-
subtitle = void 0,
|
|
16
|
-
print = false,
|
|
17
|
-
tags = void 0,
|
|
18
|
-
getImage = void 0,
|
|
19
|
-
setImage = void 0,
|
|
20
|
-
save = null,
|
|
21
|
-
outline,
|
|
22
|
-
before,
|
|
23
|
-
after
|
|
24
|
-
} = $props();
|
|
10
|
+
let { editable, label, header, id, subtitle = undefined, print = false, tags = undefined, getImage = undefined, setImage = undefined, save = null, outline, before, after } = $props();
|
|
25
11
|
let title = $state(null);
|
|
26
12
|
let showReminder = $state(true);
|
|
27
13
|
let auth = getUser();
|
|
28
14
|
let edition = getEdition();
|
|
29
15
|
function updateScrollReminder() {
|
|
30
|
-
|
|
31
|
-
|
|
16
|
+
// If the bottom of the window is below the top of the title, hide the reminder.
|
|
17
|
+
if (title) showReminder = window.scrollY + window.innerHeight <= title.getBoundingClientRect().top + window.scrollY;
|
|
32
18
|
}
|
|
33
19
|
function addCover() {
|
|
34
|
-
|
|
20
|
+
if (setImage) setImage("|||||");
|
|
35
21
|
}
|
|
36
22
|
function removeCover() {
|
|
37
|
-
|
|
23
|
+
if (setImage) setImage(null);
|
|
38
24
|
}
|
|
25
|
+
// When the title becomes visible or hidden, update the scroll reminder.
|
|
39
26
|
onMount(() => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
};
|
|
27
|
+
const intersectionObserver = new IntersectionObserver(() => updateScrollReminder());
|
|
28
|
+
if (title) intersectionObserver.observe(title);
|
|
29
|
+
updateScrollReminder();
|
|
30
|
+
// Stop observing when unmounted.
|
|
31
|
+
return () => {
|
|
32
|
+
if (title) intersectionObserver.unobserve(title);
|
|
33
|
+
};
|
|
48
34
|
});
|
|
35
|
+
// Get the embed, update when getImage function prop changes.
|
|
49
36
|
let embed = $derived(getImage ? getImage() : null);
|
|
50
37
|
</script>
|
|
51
38
|
|