bookish-press 0.4.23 → 0.4.25
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/components/Auth.svelte +11 -13
- package/components/BookPreview.svelte +12 -14
- package/components/app/Feedback.svelte +12 -10
- package/components/app/Head.svelte +4 -1
- package/components/app/Head.svelte.d.ts +6 -15
- package/components/app/Paragraph.svelte +1 -0
- package/components/app/{Writing.svelte → Section.svelte} +0 -1
- package/components/app/Section.svelte.d.ts +27 -0
- package/components/app/TextInput.svelte +1 -0
- package/components/chapter/Block.svelte +3 -1
- package/components/chapter/CaptionedCode.svelte +2 -2
- package/components/chapter/ChapterBody.svelte +2 -1
- package/components/chapter/ChapterBody.svelte.d.ts +1 -0
- package/components/chapter/Comment.svelte +2 -2
- package/components/chapter/Embed.svelte +3 -3
- package/components/chapter/Embed.svelte.d.ts +1 -0
- package/components/chapter/Figure.svelte +2 -2
- package/components/chapter/Format.svelte +5 -3
- package/components/chapter/Format.svelte.d.ts +1 -0
- package/components/chapter/InlineCode.svelte +2 -2
- package/components/chapter/Label.svelte +2 -2
- package/components/chapter/Link.svelte +2 -2
- package/components/chapter/Marginal.svelte +2 -2
- package/components/chapter/Paragraph.svelte +5 -0
- package/components/chapter/Table.svelte +2 -2
- package/components/chapter/Text.svelte +2 -2
- package/components/editor/BookishEditor.svelte +51 -28
- package/components/editor/BookishEditor.svelte.d.ts +4 -1
- package/components/editor/CaretView.svelte +3 -1
- package/components/editor/CaretView.svelte.d.ts +1 -0
- package/components/editor/ConfirmButton.svelte +1 -1
- package/components/editor/Note.svelte +1 -0
- package/components/editor/Permissions.svelte +95 -0
- package/components/editor/Permissions.svelte.d.ts +20 -0
- package/components/editor/Toolbar.svelte +0 -1
- package/components/editor/ToolbarGroup.svelte +0 -1
- package/components/page/Acknowledgements.svelte +13 -11
- package/components/page/Authors.svelte +5 -8
- package/components/page/Authors.svelte.d.ts +1 -0
- package/components/page/BookLoader.svelte +59 -0
- package/components/page/BookLoader.svelte.d.ts +16 -0
- package/components/page/BulkReferenceEditor.svelte +8 -8
- package/components/page/Chapter.svelte +43 -10
- package/components/page/Contexts.d.ts +14 -5
- package/components/page/Contexts.js +88 -6
- package/components/page/DefinitionView.svelte +18 -8
- package/components/page/Description.svelte +12 -9
- package/components/page/Edition.svelte +8 -8
- package/components/page/Edition.svelte.d.ts +0 -1
- package/components/page/EditionLoader.svelte +99 -0
- package/components/page/EditionLoader.svelte.d.ts +18 -0
- package/components/page/Editions.svelte +92 -143
- package/components/page/Glossary.svelte +13 -6
- package/components/page/Header.svelte +22 -20
- package/components/page/Header.svelte.d.ts +2 -0
- package/components/page/Index.svelte +6 -4
- package/components/page/Instructions.svelte +1 -2
- package/components/page/Instructions.svelte.d.ts +3 -1
- package/components/page/License.svelte +20 -12
- package/components/page/Media.svelte +14 -12
- package/components/page/Publish.svelte +120 -0
- package/components/page/Publish.svelte.d.ts +14 -0
- package/components/page/Reference.svelte +16 -17
- package/components/page/References.svelte +8 -3
- package/components/page/Search.svelte +3 -1
- package/components/page/Status.svelte +18 -19
- package/components/page/SubdomainEditor.svelte +3 -2
- package/components/page/TableOfContents.svelte +70 -30
- package/components/page/TableOfContentsRow.svelte +4 -4
- package/components/page/ThemeEditor.svelte +8 -5
- package/components/page/Unknown.svelte +5 -2
- package/models/CRUD.d.ts +19 -11
- package/models/CRUD.js +177 -122
- package/models/Firebase.js +12 -8
- package/models/book/Book.d.ts +19 -14
- package/models/book/Book.js +47 -54
- package/models/book/Chapter.d.ts +5 -1
- package/models/book/Chapter.js +24 -15
- package/models/book/Edition.d.ts +31 -7
- package/models/book/Edition.js +101 -32
- package/models/chapter/Parser.d.ts +2 -2
- package/models/chapter/Parser.js +26 -24
- package/package.json +18 -15
- package/components/app/Writing.svelte.d.ts +0 -27
- package/models/loadEdition.d.ts +0 -5
- package/models/loadEdition.js +0 -20
|
@@ -3,14 +3,17 @@ import Outline from './Outline.svelte';
|
|
|
3
3
|
import Page from './Page.svelte';
|
|
4
4
|
import Instructions from './Instructions.svelte';
|
|
5
5
|
import ChapterIDs from '../../models/book/ChapterID';
|
|
6
|
-
import { getEdition } from './Contexts';
|
|
6
|
+
import { getEdition, isEditionEditable } from './Contexts';
|
|
7
|
+
let editable = isEditionEditable();
|
|
7
8
|
let edition = getEdition();
|
|
8
9
|
</script>
|
|
9
10
|
|
|
10
11
|
{#if $edition}
|
|
11
12
|
<Page title={`${$edition.getTitle()} - Unknown Page`}>
|
|
12
13
|
<Header
|
|
14
|
+
editable={isEditionEditable()}
|
|
13
15
|
label="Unknown page title"
|
|
16
|
+
id="unknown"
|
|
14
17
|
getImage={() => $edition?.getImage(ChapterIDs.UnknownID) ?? null}
|
|
15
18
|
setImage={(embed) =>
|
|
16
19
|
$edition
|
|
@@ -23,7 +26,7 @@ let edition = getEdition();
|
|
|
23
26
|
<Outline slot="outline" previous={null} next={null} />
|
|
24
27
|
</Header>
|
|
25
28
|
|
|
26
|
-
<Instructions>
|
|
29
|
+
<Instructions {editable}>
|
|
27
30
|
This page will be shown if the reader somehow ends up on a page that
|
|
28
31
|
doesn't exist. You can customize the image shown.
|
|
29
32
|
</Instructions>
|
package/models/CRUD.d.ts
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { DocumentReference } from 'firebase/firestore';
|
|
2
2
|
import Edition from './book/Edition';
|
|
3
3
|
import Book from './book/Book';
|
|
4
|
-
import type {
|
|
5
|
-
export declare function
|
|
6
|
-
export declare function
|
|
7
|
-
export declare function
|
|
8
|
-
export declare function
|
|
9
|
-
export declare function
|
|
10
|
-
export declare
|
|
4
|
+
import type { Unsubscribe } from 'firebase/auth';
|
|
5
|
+
export declare function listenToPublishedBooks(react: (books: Book[]) => void): Unsubscribe;
|
|
6
|
+
export declare function listenToEditableBooks(userID: string, react: (books: Book[]) => void): Unsubscribe;
|
|
7
|
+
export declare function listenToPartiallyEditableBooks(userID: string, react: (books: Book[]) => void): Unsubscribe;
|
|
8
|
+
export declare function listenToBooksByName(name: string, react: (book: Book[]) => void): Unsubscribe;
|
|
9
|
+
export declare function listenToBookWithID(id: string, react: (book: Book | null) => void): Unsubscribe;
|
|
10
|
+
export declare function listenToEdition(bookID: string, editionID: string, react: (edition: Edition | null) => void): Unsubscribe;
|
|
11
|
+
export declare function listenToChapters(bookID: string, editionID: string, react: (chapters: [DocumentReference, string][]) => void): Unsubscribe;
|
|
12
|
+
/** Hit the authentication database to get user emails. */
|
|
13
|
+
export declare function getUserEmails(uids: string[]): Promise<Map<string, string> | null>;
|
|
14
|
+
export declare function createUser(email: string): Promise<string | null>;
|
|
15
|
+
export declare function createBook(userID: string): Promise<string>;
|
|
11
16
|
export declare function createNewEdition(book: Book): Promise<Book>;
|
|
12
|
-
export declare
|
|
17
|
+
export declare function updateLock(edition: Edition, uid: string, content: string, lock: boolean): Promise<boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* Persists the edition in the database, creating and deleting chapters as necessary.
|
|
20
|
+
* returns a map of DocumentReferences for new chapters so they can be persisted client side.
|
|
21
|
+
*/
|
|
22
|
+
export declare function updateEdition(previousEdition: Edition | undefined, newEdition: Edition): Promise<Map<string, DocumentReference>>;
|
|
13
23
|
export declare function updateBook(book: Book): Promise<void>;
|
|
14
|
-
export declare function readChapterText(chapterRef: DocumentReference): Promise<ChapterContent>;
|
|
15
24
|
export declare function isSubdomainAvailable(subdomain: string, book: Book): Promise<boolean>;
|
|
16
|
-
export declare function
|
|
17
|
-
export declare function publish(book: Book, index: number): Promise<string | undefined>;
|
|
25
|
+
export declare function publish(edition: Edition): Promise<string | undefined>;
|
package/models/CRUD.js
CHANGED
|
@@ -1,146 +1,201 @@
|
|
|
1
1
|
import { db, functions } from './Firebase';
|
|
2
|
-
import { collection, getDocs, getDoc, setDoc, doc, addDoc, query, where, deleteDoc, DocumentReference, } from 'firebase/firestore';
|
|
3
|
-
import {
|
|
2
|
+
import { collection, getDocs, getDoc, setDoc, doc, addDoc, query, where, deleteDoc, DocumentReference, onSnapshot, QueryFieldFilterConstraint, } from 'firebase/firestore';
|
|
3
|
+
import { httpsCallable } from 'firebase/functions';
|
|
4
4
|
import Edition from './book/Edition';
|
|
5
|
-
import Book from './book/Book';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
throw Error("Can't retrieve books, not connected to the database.");
|
|
9
|
-
try {
|
|
10
|
-
const data = await getDocs(collection(db, 'books'));
|
|
11
|
-
return data.docs
|
|
12
|
-
.map((doc) => Book.fromJSON(doc.ref, doc.data()))
|
|
13
|
-
.filter((book) => book.hasPublishedEdition());
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
throw Error("Couldn't load books, problem reading from the database.");
|
|
17
|
-
}
|
|
5
|
+
import Book, {} from './book/Book';
|
|
6
|
+
function editionPath(bookID, editionID) {
|
|
7
|
+
return `books/${bookID}/editions/${editionID}`;
|
|
18
8
|
}
|
|
19
|
-
|
|
9
|
+
const NO_DATABASE_CONNECTION = 'Not connected to database';
|
|
10
|
+
///////////////
|
|
11
|
+
//// READ BOOKS
|
|
12
|
+
///////////////
|
|
13
|
+
function listenToBooks(constraint, react) {
|
|
20
14
|
if (!db)
|
|
21
|
-
throw
|
|
15
|
+
throw NO_DATABASE_CONNECTION;
|
|
22
16
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
return onSnapshot(query(collection(db, 'books'), constraint), (snapshot) => react(snapshot.empty
|
|
18
|
+
? []
|
|
19
|
+
: snapshot.docs.map((doc) => Book.fromJSON(doc.ref, doc.data()))));
|
|
25
20
|
}
|
|
26
21
|
catch (err) {
|
|
27
|
-
|
|
28
|
-
return null;
|
|
22
|
+
throw 'Issue querying database';
|
|
29
23
|
}
|
|
30
24
|
}
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
export function listenToPublishedBooks(react) {
|
|
26
|
+
return listenToBooks(where('published', '==', true), react);
|
|
27
|
+
}
|
|
28
|
+
export function listenToEditableBooks(userID, react) {
|
|
29
|
+
return listenToBooks(where('uids', 'array-contains', userID), react);
|
|
30
|
+
}
|
|
31
|
+
export function listenToPartiallyEditableBooks(userID, react) {
|
|
32
|
+
return listenToBooks(where('readuids', 'array-contains', userID), react);
|
|
37
33
|
}
|
|
38
|
-
export
|
|
34
|
+
export function listenToBooksByName(name, react) {
|
|
35
|
+
return listenToBooks(where('domain', '==', name), react);
|
|
36
|
+
}
|
|
37
|
+
export function listenToBookWithID(id, react) {
|
|
39
38
|
if (!db)
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
throw NO_DATABASE_CONNECTION;
|
|
40
|
+
const ref = doc(db, 'books', id);
|
|
41
|
+
return onSnapshot(doc(db, 'books', id), (doc) => {
|
|
42
|
+
if (!doc.metadata.hasPendingWrites)
|
|
43
|
+
react(doc.exists()
|
|
44
|
+
? Book.fromJSON(ref, doc.data())
|
|
45
|
+
: null);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function listenToEdition(bookID, editionID, react) {
|
|
48
49
|
if (!db)
|
|
49
|
-
throw
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
50
|
+
throw NO_DATABASE_CONNECTION;
|
|
51
|
+
return onSnapshot(doc(db, 'books', bookID, 'editions', editionID), (doc) => {
|
|
52
|
+
if (!doc.metadata.hasPendingWrites)
|
|
53
|
+
react(doc.exists()
|
|
54
|
+
? Edition.fromJSON(doc.ref, doc.data())
|
|
55
|
+
: null);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function listenToChapters(bookID, editionID, react) {
|
|
59
|
+
if (!db)
|
|
60
|
+
throw NO_DATABASE_CONNECTION;
|
|
61
|
+
return onSnapshot(collection(db, 'books', bookID, 'editions', editionID, 'chapters'), (docs) => {
|
|
62
|
+
const chapters = [];
|
|
63
|
+
docs.forEach((doc) => {
|
|
64
|
+
chapters.push([doc.ref, doc.data().text]);
|
|
65
|
+
});
|
|
66
|
+
react(chapters);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/////////////////
|
|
70
|
+
//// MANAGE USERS
|
|
71
|
+
/////////////////
|
|
72
|
+
/** Hit the authentication database to get user emails. */
|
|
73
|
+
export async function getUserEmails(uids) {
|
|
74
|
+
if (!functions)
|
|
75
|
+
throw Error("Can't publish, no connection to Firebase functions.");
|
|
76
|
+
const getUserEmails = httpsCallable(functions, 'getUserEmails');
|
|
77
|
+
const result = await getUserEmails({
|
|
78
|
+
uids,
|
|
79
|
+
});
|
|
80
|
+
return new Map(Object.entries(result.data));
|
|
81
|
+
}
|
|
82
|
+
export async function createUser(email) {
|
|
83
|
+
if (!functions)
|
|
84
|
+
throw Error("Can't publish, no connection to Firebase functions.");
|
|
85
|
+
const createUserWithEmail = httpsCallable(functions, 'createUserWithEmail');
|
|
86
|
+
const result = await createUserWithEmail({
|
|
87
|
+
email,
|
|
88
|
+
});
|
|
89
|
+
return result.data;
|
|
63
90
|
}
|
|
64
|
-
export
|
|
91
|
+
export async function createBook(userID) {
|
|
65
92
|
if (!db)
|
|
66
93
|
throw Error("Can't create book, not connected to Firebase.");
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const editionRef = await addDoc(collection(db, 'editions'), newEdition.toObject());
|
|
71
|
-
// Remember the ref.
|
|
72
|
-
newEdition = newEdition.withRef(editionRef);
|
|
73
|
-
// Note that we don't create a sub-collection of chapters here.
|
|
74
|
-
// Firestore creates sub-collections automatically when documents are added.
|
|
75
|
-
// Create a new book with the draft first edition and the user ID.
|
|
76
|
-
const newBook = {
|
|
77
|
-
title: '',
|
|
94
|
+
// Create a new book
|
|
95
|
+
const newBookData = {
|
|
96
|
+
title: 'Untitled',
|
|
78
97
|
authors: [],
|
|
79
98
|
description: '',
|
|
80
99
|
cover: null,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
time: Date.now(),
|
|
85
|
-
summary: '',
|
|
86
|
-
published: false,
|
|
87
|
-
},
|
|
88
|
-
],
|
|
100
|
+
published: false,
|
|
101
|
+
domain: null,
|
|
102
|
+
editions: [],
|
|
89
103
|
uids: [userID],
|
|
104
|
+
readuids: [],
|
|
90
105
|
};
|
|
91
106
|
// Create the book
|
|
92
|
-
const bookRef = await addDoc(collection(db, 'books'),
|
|
107
|
+
const bookRef = await addDoc(collection(db, 'books'), newBookData);
|
|
108
|
+
// Create a model to store it.
|
|
109
|
+
let book = Book.fromJSON(bookRef, newBookData);
|
|
110
|
+
// Make a new default edition with a single chapter with this user id.
|
|
111
|
+
let newEdition = new Edition(bookRef, undefined, [], 'Untitled', [], 1, '', null, {}, '', [], 'All rights reserved.', '', [], {}, {}, {}, {}, null, {}).withNewChapter();
|
|
112
|
+
// Add the edition to the editions collection in the database so we can get an edition ref.
|
|
113
|
+
const editionRef = await addDoc(collection(db, 'books', bookRef.id, 'editions'), newEdition.toObject());
|
|
114
|
+
// Remember the ref.
|
|
115
|
+
newEdition = newEdition.withRef(editionRef);
|
|
116
|
+
// Now replicate the edition info in the book. We dodge type checking here because we know with certainty
|
|
117
|
+
// that the ref was just added the line before.
|
|
118
|
+
const info = newEdition.getInfo();
|
|
119
|
+
if (info)
|
|
120
|
+
book = book.withEditions([info]);
|
|
121
|
+
// Update the book with the edition info.
|
|
122
|
+
await updateBook(book);
|
|
93
123
|
// Update all of the changes to the edition in Firestore, persisting the refs and chapter text.
|
|
94
124
|
await updateEdition(undefined, newEdition);
|
|
95
125
|
// Return the book id
|
|
96
126
|
return bookRef.id;
|
|
97
|
-
}
|
|
127
|
+
}
|
|
98
128
|
export async function createNewEdition(book) {
|
|
99
129
|
if (!db)
|
|
100
130
|
throw Error("Can't publish draft, no Firebase connection.");
|
|
101
131
|
// Get the latest draft.
|
|
102
|
-
const
|
|
132
|
+
const latestEditionID = book.getLatestEditionID();
|
|
133
|
+
if (latestEditionID === undefined)
|
|
134
|
+
throw 'No editions on this book';
|
|
135
|
+
const latestDraftDoc = await getDoc(doc(db, 'books', book.ref.id, 'editions', latestEditionID));
|
|
136
|
+
const latestDraft = latestDraftDoc.exists()
|
|
137
|
+
? latestDraftDoc.data()
|
|
138
|
+
: undefined;
|
|
103
139
|
if (latestDraft === undefined)
|
|
104
140
|
return book;
|
|
105
|
-
// Make a copy without chapter and edition refs, because we want to create new ones
|
|
141
|
+
// Make a copy without chapter and edition refs, because we want to create new ones,
|
|
142
|
+
// and with an incremented edition number.
|
|
106
143
|
const newEdition = latestDraft.withoutRefs();
|
|
107
|
-
// Create a copy of the existing draft as a new edition
|
|
144
|
+
// Create a copy of the existing draft as a new edition.
|
|
108
145
|
const newEditionRef = await addDoc(collection(db, 'editions'), newEdition.toObject());
|
|
109
|
-
//
|
|
146
|
+
// Create the edition, creating chapter text as necessary, using the edition sync function.
|
|
110
147
|
updateEdition(undefined, newEdition.withRef(newEditionRef));
|
|
111
148
|
// Put the new edition in the book.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
published: false,
|
|
118
|
-
});
|
|
119
|
-
book = book.withRevisions(revisions);
|
|
120
|
-
// Return the revised Book's revision.'
|
|
149
|
+
book = book.withEditions([
|
|
150
|
+
newEdition.getInfo(),
|
|
151
|
+
...book.getEditions(),
|
|
152
|
+
]);
|
|
153
|
+
// Update the book.
|
|
121
154
|
updateBook(book);
|
|
122
155
|
return book;
|
|
123
156
|
}
|
|
124
|
-
export
|
|
157
|
+
export async function updateLock(edition, uid, content, lock) {
|
|
158
|
+
if (!db)
|
|
159
|
+
throw NO_DATABASE_CONNECTION;
|
|
160
|
+
if (edition.editionRef === undefined)
|
|
161
|
+
return false;
|
|
162
|
+
if (lock && edition.hasLease(uid, content))
|
|
163
|
+
return true;
|
|
164
|
+
if (!lock && !edition.hasLease(uid, content))
|
|
165
|
+
return true;
|
|
166
|
+
await setDoc(doc(db, editionPath(edition.bookRef.id, edition.editionRef.id)), edition.withLock(uid, content, lock).toObject());
|
|
167
|
+
return lock;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Persists the edition in the database, creating and deleting chapters as necessary.
|
|
171
|
+
* returns a map of DocumentReferences for new chapters so they can be persisted client side.
|
|
172
|
+
*/
|
|
173
|
+
export async function updateEdition(previousEdition, newEdition) {
|
|
125
174
|
if (!db)
|
|
126
175
|
throw Error("Can't update edition, not connected to Firebase.");
|
|
127
176
|
// Get the reference to the edition, fail if we don't have one.
|
|
128
|
-
const editionRef = newEdition.
|
|
177
|
+
const editionRef = newEdition.getEditionRef();
|
|
129
178
|
if (!editionRef)
|
|
130
179
|
throw Error('Book given has no ID');
|
|
180
|
+
const newChapterRefs = new Map();
|
|
131
181
|
// Create any new chapters that aren't in the current edition, and update text if it changed.
|
|
132
182
|
for (const newChapter of newEdition.chapters) {
|
|
133
183
|
if (newChapter.ref === undefined) {
|
|
134
184
|
// Make the new chapter's text.
|
|
135
|
-
const newChapterRef = await addDoc(collection(db,
|
|
185
|
+
const newChapterRef = await addDoc(collection(db, editionPath(newEdition.bookRef.id, editionRef.id), 'chapters'), { text: newChapter.text });
|
|
136
186
|
// Add the ref of the new chapter to the revised chapter.
|
|
137
187
|
newEdition = newEdition.withRevisedChapter(newChapter, newChapter.withRef(newChapterRef));
|
|
188
|
+
// Map the chapter ref to it's id.
|
|
189
|
+
newChapterRefs.set(newChapter.id, newChapterRef);
|
|
138
190
|
}
|
|
139
191
|
// If we have a previous version, did the text change? If so, update it.
|
|
140
|
-
else
|
|
141
|
-
const previousChapter = previousEdition
|
|
142
|
-
|
|
143
|
-
|
|
192
|
+
else {
|
|
193
|
+
const previousChapter = previousEdition === undefined
|
|
194
|
+
? undefined
|
|
195
|
+
: previousEdition.getChapterByRef(newChapter.ref);
|
|
196
|
+
if (previousChapter === undefined ||
|
|
197
|
+
newChapter.text !== previousChapter.text) {
|
|
198
|
+
await setDoc(doc(db, editionPath(newEdition.bookRef.id, editionRef.id), 'chapters', newChapter.ref.id), { text: newChapter.text });
|
|
144
199
|
}
|
|
145
200
|
}
|
|
146
201
|
}
|
|
@@ -153,53 +208,53 @@ export const updateEdition = async (previousEdition, newEdition) => {
|
|
|
153
208
|
}
|
|
154
209
|
}
|
|
155
210
|
}
|
|
211
|
+
// If the edition's summary info changed, update it.
|
|
212
|
+
if (previousEdition !== undefined) {
|
|
213
|
+
const previousInfo = previousEdition.getInfo();
|
|
214
|
+
const newInfo = newEdition.getInfo();
|
|
215
|
+
if (previousInfo && newInfo) {
|
|
216
|
+
const previousKeys = Object.keys(previousInfo);
|
|
217
|
+
const newKeys = Object.keys(newInfo);
|
|
218
|
+
if (previousKeys.length !== newKeys.length ||
|
|
219
|
+
!previousKeys.every((key) => previousInfo[key] === newInfo[key])) {
|
|
220
|
+
const bookDoc = await getDoc(doc(db, 'books', newEdition.bookRef.id));
|
|
221
|
+
if (!bookDoc.exists())
|
|
222
|
+
throw Error('Book does not exist');
|
|
223
|
+
const book = Book.fromJSON(newEdition.bookRef, bookDoc.data());
|
|
224
|
+
updateBook(book.withRevisedEdition(previousEdition, newEdition));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
156
228
|
// Finally, update the edition's document.
|
|
157
|
-
await setDoc(doc(db,
|
|
158
|
-
return
|
|
159
|
-
}
|
|
229
|
+
await setDoc(doc(db, editionPath(newEdition.bookRef.id, editionRef.id)), newEdition.toObject());
|
|
230
|
+
return newChapterRefs;
|
|
231
|
+
}
|
|
160
232
|
export async function updateBook(book) {
|
|
161
233
|
if (!db)
|
|
162
234
|
throw Error("Can't update book, not connected to Firebase.");
|
|
163
235
|
await setDoc(doc(db, 'books', book.getRefID()), book.toJSON());
|
|
164
236
|
}
|
|
165
|
-
export async function readChapterText(chapterRef) {
|
|
166
|
-
if (!db)
|
|
167
|
-
throw Error('Not connected to Firebase.');
|
|
168
|
-
const text = await getDoc(chapterRef);
|
|
169
|
-
if (!text.exists())
|
|
170
|
-
throw Error('Chapter text does not exist.');
|
|
171
|
-
return text.data();
|
|
172
|
-
}
|
|
173
237
|
export async function isSubdomainAvailable(subdomain, book) {
|
|
174
238
|
if (!db)
|
|
175
239
|
throw Error('Not connected to Firebase.');
|
|
176
240
|
const matches = await getDocs(query(collection(db, 'books'), where('domain', '==', subdomain)));
|
|
177
241
|
return matches.empty || matches.docs[0].id === book.getRefID();
|
|
178
242
|
}
|
|
179
|
-
export async function
|
|
180
|
-
if (!db)
|
|
181
|
-
throw Error("Can't find book ID from URL, not connected to Firebase.");
|
|
182
|
-
const matches = await getDocs(query(collection(db, 'books'), where('domain', '==', subdomain)));
|
|
183
|
-
if (!matches.empty)
|
|
184
|
-
return matches.docs[0].id;
|
|
185
|
-
else
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
export async function publish(book, index) {
|
|
243
|
+
export async function publish(edition) {
|
|
189
244
|
if (!db)
|
|
190
|
-
throw Error(
|
|
245
|
+
throw Error('Cannot publish, no database connection');
|
|
191
246
|
if (!functions)
|
|
192
|
-
throw Error(
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
return `Couldn't find edition ${index} number to publish`;
|
|
196
|
-
const editionRef = edition.getRef();
|
|
247
|
+
throw Error('Cannot publish, no cloud connection');
|
|
248
|
+
const editionRef = edition.getEditionRef();
|
|
249
|
+
const bookRef = edition.getBookRef();
|
|
197
250
|
if (editionRef === undefined)
|
|
198
|
-
return `
|
|
251
|
+
return `Cannot publish, no edition ID`;
|
|
252
|
+
// Get a reference to the publishing function
|
|
199
253
|
const publishEdition = httpsCallable(functions, 'publishEdition');
|
|
254
|
+
// Call the function, passing
|
|
200
255
|
const result = await publishEdition({
|
|
201
|
-
|
|
202
|
-
|
|
256
|
+
bookID: bookRef.id,
|
|
257
|
+
editionID: editionRef.id,
|
|
203
258
|
});
|
|
204
259
|
return result.data;
|
|
205
260
|
// Update the edition's published status in the book.
|
package/models/Firebase.js
CHANGED
|
@@ -3,24 +3,28 @@ import { connectAuthEmulator, getAuth } from 'firebase/auth';
|
|
|
3
3
|
import { connectFirestoreEmulator, getFirestore } from 'firebase/firestore';
|
|
4
4
|
import { connectFunctionsEmulator, getFunctions } from 'firebase/functions';
|
|
5
5
|
import { connectStorageEmulator, getStorage } from 'firebase/storage';
|
|
6
|
-
|
|
6
|
+
import { PUBLIC_READER, PUBLIC_CONTEXT, PUBLIC_FIREBASE_API_KEY, PUBLIC_FIREBASE_AUTH_DOMAIN, PUBLIC_FIREBASE_PROJECT_ID, PUBLIC_FIREBASE_STORAGE_BUCKET, PUBLIC_FIREBASE_MESSAGING_SENDER_ID, PUBLIC_FIREBASE_APP_ID, } from '$env/static/public';
|
|
7
|
+
const isReader = PUBLIC_READER === 'true';
|
|
8
|
+
// Some feedback about context
|
|
9
|
+
if (PUBLIC_CONTEXT !== 'prod')
|
|
10
|
+
console.log(`*** ${PUBLIC_CONTEXT.toLocaleUpperCase()} ***`);
|
|
7
11
|
// Initialize Firebase using the environment variables provided at build time.
|
|
8
12
|
// Only do this if we have environment variables defined. (We won't in the standalone Reader).
|
|
9
13
|
export const app = isReader
|
|
10
14
|
? undefined
|
|
11
15
|
: initializeApp({
|
|
12
|
-
apiKey:
|
|
13
|
-
authDomain:
|
|
14
|
-
projectId:
|
|
15
|
-
storageBucket:
|
|
16
|
-
messagingSenderId:
|
|
17
|
-
appId:
|
|
16
|
+
apiKey: PUBLIC_FIREBASE_API_KEY,
|
|
17
|
+
authDomain: PUBLIC_FIREBASE_AUTH_DOMAIN,
|
|
18
|
+
projectId: PUBLIC_FIREBASE_PROJECT_ID,
|
|
19
|
+
storageBucket: PUBLIC_FIREBASE_STORAGE_BUCKET,
|
|
20
|
+
messagingSenderId: PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
|
|
21
|
+
appId: PUBLIC_FIREBASE_APP_ID,
|
|
18
22
|
});
|
|
19
23
|
export const auth = isReader ? undefined : getAuth();
|
|
20
24
|
export const db = isReader ? undefined : getFirestore();
|
|
21
25
|
export const storage = isReader ? undefined : getStorage();
|
|
22
26
|
export const functions = isReader ? undefined : getFunctions();
|
|
23
|
-
if (!isReader &&
|
|
27
|
+
if (!isReader && PUBLIC_CONTEXT === 'local') {
|
|
24
28
|
if (db)
|
|
25
29
|
connectFirestoreEmulator(db, 'localhost', 8080);
|
|
26
30
|
if (auth)
|
package/models/book/Book.d.ts
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import type { DocumentReference } from 'firebase/firestore';
|
|
2
2
|
import BookMedia from './BookMedia';
|
|
3
3
|
import type Edition from './Edition';
|
|
4
|
+
/** A replication of edition info */
|
|
4
5
|
export type EditionInfo = {
|
|
5
6
|
ref: DocumentReference;
|
|
6
|
-
time: number;
|
|
7
7
|
summary: string;
|
|
8
|
-
|
|
8
|
+
number: number;
|
|
9
|
+
published: number | null;
|
|
10
|
+
editionuids: string[];
|
|
11
|
+
chapteruids: string[];
|
|
9
12
|
};
|
|
10
13
|
export type BookSpecification = {
|
|
11
14
|
title: string;
|
|
12
15
|
authors: string[];
|
|
13
16
|
description: string;
|
|
14
17
|
cover: string | null;
|
|
18
|
+
published: boolean;
|
|
15
19
|
editions: EditionInfo[];
|
|
16
|
-
domain
|
|
20
|
+
domain: string | null;
|
|
17
21
|
uids: string[];
|
|
22
|
+
readuids: string[];
|
|
18
23
|
};
|
|
19
24
|
export default class Book {
|
|
20
25
|
readonly ref: DocumentReference;
|
|
@@ -22,15 +27,16 @@ export default class Book {
|
|
|
22
27
|
readonly authors: string[];
|
|
23
28
|
readonly description: string;
|
|
24
29
|
readonly cover: string | null;
|
|
30
|
+
readonly published: boolean;
|
|
25
31
|
readonly editions: EditionInfo[];
|
|
26
|
-
readonly domain: string |
|
|
32
|
+
readonly domain: string | null;
|
|
27
33
|
readonly uids: string[];
|
|
28
34
|
media: BookMedia;
|
|
29
|
-
constructor(ref: DocumentReference, title: string, authors: string[], description: string, cover: string | null,
|
|
35
|
+
constructor(ref: DocumentReference, title: string, authors: string[], description: string, cover: string | null, editions: EditionInfo[], domain: string | null, uids: string[]);
|
|
30
36
|
getMedia(): BookMedia;
|
|
31
37
|
static fromJSON(ref: DocumentReference, book: BookSpecification): Book;
|
|
32
38
|
toJSON(): BookSpecification;
|
|
33
|
-
getSubdomain(): string |
|
|
39
|
+
getSubdomain(): string | null;
|
|
34
40
|
withSubdomain(domain: string): Book;
|
|
35
41
|
getTitle(): string;
|
|
36
42
|
withTitle(title: string): Book;
|
|
@@ -42,15 +48,14 @@ export default class Book {
|
|
|
42
48
|
withAuthors(authors: string[]): Book;
|
|
43
49
|
getRef(): DocumentReference<import("@firebase/firestore").DocumentData>;
|
|
44
50
|
getRefID(): string;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
getEditionNumber(number: number): Promise<Edition> | undefined;
|
|
48
|
-
getLatestPublishedEdition(): undefined | Promise<Edition>;
|
|
49
|
-
getLatestPublishedEditionID(): string | undefined;
|
|
51
|
+
getEditionNumberID(number: number): string | undefined;
|
|
52
|
+
getLatestPublishedEditionID(): string;
|
|
50
53
|
getLatestEditionID(): string | undefined;
|
|
51
54
|
getEditions(): EditionInfo[];
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
getPublishedEditionCount(): number;
|
|
56
|
+
isEditor(uid: string): boolean;
|
|
57
|
+
withEditors(uids: string[]): Book;
|
|
58
|
+
withEditions(revisions: EditionInfo[]): Book;
|
|
59
|
+
withRevisedEdition(previous: Edition, revised: Edition): Book;
|
|
55
60
|
hasPublishedEdition(): boolean;
|
|
56
61
|
}
|