bookish-press 0.7.1 → 0.7.4
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/editor/Permissions.svelte +19 -17
- package/dist/components/page/Chapter.svelte +9 -8
- package/dist/components/page/Editions.svelte +52 -27
- package/dist/components/page/Outline.svelte +1 -0
- package/dist/components/page/Page.svelte +5 -3
- package/dist/components/page/Page.svelte.d.ts +1 -1
- package/dist/components/page/Print.svelte +17 -0
- package/dist/models/CRUD.js +23 -4
- package/dist/models/book/Book.d.ts +1 -1
- package/dist/models/book/Book.js +3 -3
- package/package.json +3 -4
|
@@ -60,23 +60,25 @@ function hasEditor(email) {
|
|
|
60
60
|
</form>
|
|
61
61
|
{/if}
|
|
62
62
|
|
|
63
|
-
{#
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
63
|
+
{#key $emails}
|
|
64
|
+
{#each uids as uid, index (uid)}
|
|
65
|
+
{@const email = $emails.get(uid)}
|
|
66
|
+
<p>
|
|
67
|
+
{#if email}{email}{:else}<em>loading...</em>{/if}
|
|
68
|
+
{#if writable}
|
|
69
|
+
<Button
|
|
70
|
+
tooltip="remove {email} editing rights"
|
|
71
|
+
command={() =>
|
|
72
|
+
change([
|
|
73
|
+
...uids.slice(0, index),
|
|
74
|
+
...uids.slice(index + 1),
|
|
75
|
+
])}
|
|
76
|
+
disabled={atleastone && uids.length <= 1}>⨉</Button
|
|
77
|
+
>
|
|
78
|
+
{/if}
|
|
79
|
+
</p>
|
|
80
|
+
{/each}
|
|
81
|
+
{/key}
|
|
80
82
|
{#each inheriteduids as uid}
|
|
81
83
|
<p><em>{$emails.get(uid)}</em> <Note>book editor</Note></p>
|
|
82
84
|
{/each}
|
|
@@ -183,21 +183,22 @@ $: chapterStore.set({
|
|
|
183
183
|
marginal,
|
|
184
184
|
layoutMarginals,
|
|
185
185
|
});
|
|
186
|
-
let editable =
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
$auth
|
|
191
|
-
|
|
186
|
+
let editable = !print &&
|
|
187
|
+
(isBookEditable() ||
|
|
188
|
+
isEditionEditable() ||
|
|
189
|
+
isChapterEditable() ||
|
|
190
|
+
($auth !== undefined &&
|
|
191
|
+
$auth.user !== null &&
|
|
192
|
+
chapter.isEditor($auth.user.uid)));
|
|
192
193
|
</script>
|
|
193
194
|
|
|
194
195
|
{#if $edition}
|
|
195
196
|
<Page
|
|
196
197
|
afterLoaded={scrollToLastLocation}
|
|
197
|
-
title={`${$edition.getTitle()} - ${chapter.getTitle()}`}
|
|
198
|
+
title={print ? undefined : `${$edition.getTitle()} - ${chapter.getTitle()}`}
|
|
198
199
|
>
|
|
199
200
|
<Header
|
|
200
|
-
editable
|
|
201
|
+
{editable}
|
|
201
202
|
id={chapter.id}
|
|
202
203
|
header={chapter.getTitle()}
|
|
203
204
|
label="Chapter title"
|
|
@@ -13,20 +13,47 @@ let editable = isBookEditable();
|
|
|
13
13
|
let user = getUser();
|
|
14
14
|
$: editions = $book?.getEditions();
|
|
15
15
|
$: latestPublishedID = $book?.getLatestPublishedEditionID();
|
|
16
|
-
|
|
16
|
+
let publisher = false;
|
|
17
|
+
$: {
|
|
18
|
+
if ($user && $user.user)
|
|
19
|
+
updateUserClaims();
|
|
20
|
+
}
|
|
21
|
+
async function updateUserClaims() {
|
|
22
|
+
if ($user === undefined || $user.user === null)
|
|
23
|
+
return;
|
|
24
|
+
const token = await $user.user.getIdTokenResult();
|
|
25
|
+
publisher =
|
|
26
|
+
'publisher' in token.claims && token.claims.publisher === true;
|
|
27
|
+
}
|
|
28
|
+
async function createEdition() {
|
|
17
29
|
if ($book === undefined)
|
|
18
30
|
return;
|
|
19
|
-
|
|
31
|
+
try {
|
|
32
|
+
const revisedBook = await createNewEdition($book);
|
|
33
|
+
book.set(revisedBook);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
console.error('Error creating new edition:', err);
|
|
37
|
+
}
|
|
20
38
|
}
|
|
21
39
|
</script>
|
|
22
40
|
|
|
23
41
|
{#if $book && editions}
|
|
24
42
|
{#if editable}
|
|
25
43
|
<p>
|
|
26
|
-
<Button
|
|
27
|
-
|
|
44
|
+
<Button
|
|
45
|
+
tooltip="create new edition"
|
|
46
|
+
command={createEdition}
|
|
47
|
+
disabled={!publisher}>+ edition</Button
|
|
28
48
|
>
|
|
29
49
|
</p>
|
|
50
|
+
{#if !publisher || !$book.hasPublishedEdition()}
|
|
51
|
+
<Note
|
|
52
|
+
>You must have publisher privileges to create new editions of a
|
|
53
|
+
published book. If you believe this is an error, please contact
|
|
54
|
+
support.</Note
|
|
55
|
+
>
|
|
56
|
+
{/if}
|
|
30
57
|
{/if}
|
|
31
58
|
<Instructions {editable}>
|
|
32
59
|
Each book has one or more editions, allowing you to track revisions and
|
|
@@ -36,18 +63,17 @@ async function handleDraftEdition() {
|
|
|
36
63
|
and then publish. By default, readers will see the latest published
|
|
37
64
|
edition, but they can access older editions here.
|
|
38
65
|
</Instructions>
|
|
39
|
-
{#each editions as edition
|
|
40
|
-
{@const editionNumber =
|
|
41
|
-
editions === undefined ? -1 : editions.length - index}
|
|
66
|
+
{#each editions.toReversed() as edition (edition.ref)}
|
|
67
|
+
{@const editionNumber = edition.number}
|
|
42
68
|
{@const editionLabel =
|
|
43
69
|
editionNumber +
|
|
44
70
|
(editionNumber === 1
|
|
45
71
|
? 'st'
|
|
46
72
|
: editionNumber === 2
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
73
|
+
? 'nd'
|
|
74
|
+
: editionNumber === 3
|
|
75
|
+
? 'rd'
|
|
76
|
+
: 'th')}
|
|
51
77
|
|
|
52
78
|
{#if $book && (editable || edition.published || ($user?.user?.uid && edition.editionuids.includes($user.user.uid)))}
|
|
53
79
|
<section class="edition">
|
|
@@ -55,38 +81,37 @@ async function handleDraftEdition() {
|
|
|
55
81
|
>{editionLabel}
|
|
56
82
|
<Note
|
|
57
83
|
>{#if edition.published}{new Date(
|
|
58
|
-
edition.published
|
|
84
|
+
edition.published,
|
|
59
85
|
).toLocaleDateString(
|
|
60
|
-
'en-us'
|
|
86
|
+
'en-us',
|
|
61
87
|
)}{:else}unpublished{/if}
|
|
62
88
|
{#if edition.published}
|
|
63
89
|
<Link
|
|
64
90
|
to={latestPublishedID === edition.ref.id &&
|
|
65
91
|
$book.getSubdomain() !== undefined
|
|
66
92
|
? `/${$book.getSubdomain()}`
|
|
67
|
-
: `/${$book.getID()}/${
|
|
68
|
-
editions.length - index
|
|
69
|
-
}`}
|
|
93
|
+
: `/${$book.getID()}/${editionNumber}`}
|
|
70
94
|
external>read</Link
|
|
71
95
|
>{/if}
|
|
72
96
|
{#if editable || ($user?.user?.uid && edition.editionuids.includes($user?.user.uid))}
|
|
73
97
|
|
|
74
98
|
<Link
|
|
75
|
-
to={`/write/${$book.getID()}/${
|
|
76
|
-
|
|
77
|
-
}`}>edit</Link
|
|
99
|
+
to={`/write/${$book.getID()}/${editionNumber}`}
|
|
100
|
+
>edit</Link
|
|
78
101
|
>
|
|
79
102
|
{/if}
|
|
80
103
|
</Note>
|
|
81
104
|
</h3>
|
|
82
|
-
<p
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
105
|
+
{#if edition.summary !== ''}<p
|
|
106
|
+
><em
|
|
107
|
+
><Format
|
|
108
|
+
node={Parser.parseFormat(
|
|
109
|
+
undefined,
|
|
110
|
+
edition.summary,
|
|
111
|
+
)}
|
|
112
|
+
/></em
|
|
113
|
+
></p
|
|
114
|
+
>{/if}
|
|
90
115
|
</section>
|
|
91
116
|
{/if}
|
|
92
117
|
{/each}
|
|
@@ -324,6 +324,7 @@ let expandLabel = '\u2630';
|
|
|
324
324
|
/* Desktop */
|
|
325
325
|
@media screen and (min-width: 1200px) {
|
|
326
326
|
.outline {
|
|
327
|
+
--outline-width: min(12em, calc((100vw - 740px) / 2 - 3 * var(--outline-padding)));
|
|
327
328
|
width: var(--outline-width);
|
|
328
329
|
z-index: 0; /* Put it below everything when it's in the margin. */
|
|
329
330
|
border-right: 1px solid var(--bookish-border-color-light);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script>import { onMount } from 'svelte';
|
|
2
2
|
import Loading from './Loading.svelte';
|
|
3
|
-
export let title;
|
|
3
|
+
export let title = undefined;
|
|
4
4
|
export let afterLoaded = undefined;
|
|
5
5
|
let fontsLoaded = false;
|
|
6
6
|
let imagesLoaded = false;
|
|
@@ -44,8 +44,10 @@ function imagesAreLoaded() {
|
|
|
44
44
|
</script>
|
|
45
45
|
|
|
46
46
|
<svelte:head>
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
{#if title !== undefined}
|
|
48
|
+
<title>{title}</title>
|
|
49
|
+
<meta name="description" content={title} />
|
|
50
|
+
{/if}
|
|
49
51
|
</svelte:head>
|
|
50
52
|
|
|
51
53
|
<section class={'bookish-page' + (imagesLoaded ? ' loaded' : '')}>
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
<script>import Chapter from './Chapter.svelte';
|
|
2
2
|
import { getEdition } from './Contexts';
|
|
3
|
+
import Parser from '../../models/chapter/Parser';
|
|
3
4
|
let edition = getEdition();
|
|
5
|
+
$: title = $edition
|
|
6
|
+
? [
|
|
7
|
+
$edition.getTitle(),
|
|
8
|
+
`${$edition.getEditionLabel()} Edition`,
|
|
9
|
+
Parser.parseFormat($edition, $edition.getAuthors().at(0) ?? '').toText() + ($edition.getAuthors().length > 1 ? ' et al.' : ''),
|
|
10
|
+
]
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
.join(' - ')
|
|
13
|
+
: undefined;
|
|
4
14
|
</script>
|
|
5
15
|
|
|
16
|
+
<svelte:head>
|
|
17
|
+
{#if title}<title>{title}</title>{/if}
|
|
18
|
+
</svelte:head>
|
|
19
|
+
|
|
6
20
|
{#if $edition}
|
|
7
21
|
{#each $edition
|
|
8
22
|
.getChapters()
|
|
@@ -16,6 +30,9 @@ let edition = getEdition();
|
|
|
16
30
|
:global(.bookish-chapter-header) {
|
|
17
31
|
break-before: page;
|
|
18
32
|
}
|
|
33
|
+
:global(.bookish-page:first-of-type .bookish-chapter-header) {
|
|
34
|
+
break-before: auto;
|
|
35
|
+
}
|
|
19
36
|
|
|
20
37
|
:global(body) {
|
|
21
38
|
font-size: 16pt;
|
package/dist/models/CRUD.js
CHANGED
|
@@ -132,7 +132,7 @@ export async function createBook(userID) {
|
|
|
132
132
|
}
|
|
133
133
|
export async function createNewEdition(book) {
|
|
134
134
|
if (!db)
|
|
135
|
-
throw Error("Can't
|
|
135
|
+
throw Error("Can't create new edition, no Firebase connection.");
|
|
136
136
|
// Get the latest draft.
|
|
137
137
|
const latestEditionID = book.getLatestEditionID();
|
|
138
138
|
if (latestEditionID === undefined)
|
|
@@ -141,6 +141,7 @@ export async function createNewEdition(book) {
|
|
|
141
141
|
const latestDraftData = latestDraftDoc.exists()
|
|
142
142
|
? latestDraftDoc.data()
|
|
143
143
|
: undefined;
|
|
144
|
+
// No latest draft? Return the book as is.
|
|
144
145
|
if (latestDraftData === undefined)
|
|
145
146
|
return book;
|
|
146
147
|
const latestDraft = Edition.fromJSON(latestDraftDoc.ref, latestDraftData);
|
|
@@ -149,18 +150,36 @@ export async function createNewEdition(book) {
|
|
|
149
150
|
let newEdition = latestDraft.withoutRefs();
|
|
150
151
|
// Create a copy of the existing draft as a new edition.
|
|
151
152
|
const newEditionRef = await addDoc(collection(db, 'books', book.id, 'editions'), newEdition.toObject());
|
|
153
|
+
// Create a new edition that has the new ref and an updated edition number.
|
|
152
154
|
newEdition = newEdition
|
|
153
155
|
.withRef(newEditionRef)
|
|
154
156
|
.withIncrementedEditionNumber();
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
+
// Add the chapter text of the existing edition into the new edition's chapters, so it gets copied over. We have to do this because the chapter text is stored in the chapter documents, not the edition document.
|
|
158
|
+
const textByID = new Map();
|
|
159
|
+
for (const chapter of latestDraft.chapters) {
|
|
160
|
+
if (chapter.ref === undefined)
|
|
161
|
+
continue;
|
|
162
|
+
const chapterDoc = await getDoc(chapter.ref);
|
|
163
|
+
if (!chapterDoc.exists())
|
|
164
|
+
continue;
|
|
165
|
+
const chapterData = chapterDoc.data();
|
|
166
|
+
if (!chapterData || typeof chapterData.text !== 'string')
|
|
167
|
+
continue;
|
|
168
|
+
textByID.set(chapter.id, chapterData.text);
|
|
169
|
+
}
|
|
170
|
+
newEdition = newEdition.withChapters(newEdition.chapters.map((c) => {
|
|
171
|
+
const text = textByID.get(c.id) ?? '';
|
|
172
|
+
return c.withText(text);
|
|
173
|
+
}));
|
|
174
|
+
// Save the edition, creating chapter text as necessary, using the edition sync function.
|
|
175
|
+
await updateEdition(book, undefined, newEdition);
|
|
157
176
|
// Put the new edition in the book.
|
|
158
177
|
book = book.withEditions([
|
|
159
178
|
newEdition.getInfo(),
|
|
160
179
|
...book.getEditions(),
|
|
161
180
|
]);
|
|
162
181
|
// Update the book.
|
|
163
|
-
updateBook(book);
|
|
182
|
+
await updateBook(book);
|
|
164
183
|
return book;
|
|
165
184
|
}
|
|
166
185
|
export async function updateLock(edition, uid, content, lock) {
|
|
@@ -49,7 +49,7 @@ export default class Book {
|
|
|
49
49
|
withAuthors(authors: string[]): Book;
|
|
50
50
|
getID(): string;
|
|
51
51
|
getEditionNumberID(number: number): string | undefined;
|
|
52
|
-
getLatestPublishedEditionID(): string;
|
|
52
|
+
getLatestPublishedEditionID(): string | undefined;
|
|
53
53
|
getLatestEditionID(): string | undefined;
|
|
54
54
|
getEditions(): EditionInfo[];
|
|
55
55
|
getPublishedEditionCount(): number;
|
package/dist/models/book/Book.js
CHANGED
|
@@ -18,7 +18,7 @@ export default class Book {
|
|
|
18
18
|
this.cover = cover;
|
|
19
19
|
// The published state is always true if some edition's published flag isn't null.
|
|
20
20
|
this.published = editions.some((edition) => edition.published !== null);
|
|
21
|
-
// We always keep editions in
|
|
21
|
+
// We always keep editions in ascending edition number for easy reference.
|
|
22
22
|
this.editions = editions.sort((a, b) => a.number - b.number);
|
|
23
23
|
this.domain = domain;
|
|
24
24
|
this.uids = uids;
|
|
@@ -91,10 +91,10 @@ export default class Book {
|
|
|
91
91
|
return edition ? edition.ref.id : undefined;
|
|
92
92
|
}
|
|
93
93
|
getLatestPublishedEditionID() {
|
|
94
|
-
return this.editions.filter((ed) => ed.published !== null)
|
|
94
|
+
return this.editions.filter((ed) => ed.published !== null).at(-1)?.ref.id;
|
|
95
95
|
}
|
|
96
96
|
getLatestEditionID() {
|
|
97
|
-
return this.editions[0]?.ref.id;
|
|
97
|
+
return this.editions.toSorted((a, b) => b.number - a.number)[0]?.ref.id;
|
|
98
98
|
}
|
|
99
99
|
getEditions() {
|
|
100
100
|
return this.editions.slice();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bookish-press",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A platform for reading and writing readable, accessible, beautiful online books.",
|
|
7
7
|
"license": "MIT",
|
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
"merge": "git checkout main && git merge dev && git push origin main && git checkout dev"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@sveltejs/adapter-
|
|
31
|
-
"@sveltejs/adapter-static": "^3",
|
|
30
|
+
"@sveltejs/adapter-node": "^5",
|
|
32
31
|
"@sveltejs/kit": "^2",
|
|
33
32
|
"@sveltejs/package": "^2",
|
|
34
33
|
"@sveltejs/vite-plugin-svelte": "^3",
|
|
@@ -59,7 +58,7 @@
|
|
|
59
58
|
"uuid": "^11"
|
|
60
59
|
},
|
|
61
60
|
"engines": {
|
|
62
|
-
"node": "
|
|
61
|
+
"node": "22"
|
|
63
62
|
},
|
|
64
63
|
"types": "./dist/index.d.ts",
|
|
65
64
|
"main": "./dist/index.js",
|