feat: confirm a saved quotation with a short note

This commit is contained in:
randogoth 2026-10-12 10:37:32 +03:00
parent e6e84fdc01
commit 4d581caad5
11 changed files with 39 additions and 7 deletions

View file

@ -87,7 +87,7 @@ The v1 client presents one reader screen:
- Subscribed sites are checked once in the background at launch, and whenever the reader asks. Sites are checked one after another rather than at once, and a site that cannot be reached keeps the posts already held and is still recorded as checked, so one site being down never stops the rest.
- A fresh install comes with the project's directory of sites already bookmarked, so a first session has somewhere to go. It is a bookmark like any other: removing it is the reader's own, and no later start gives it back.
- The reader can tag bookmarks and sites. A tag exists only while something carries it, so adding a tag and making one are the same act; `mews:tags` lists every tag in use and what carries it, and following a tag narrows the list it belongs to. There is no renaming and no empty tag.
- The reader can keep marked text as a quotation. Marking text on a page offers, in the selection toolbar, to save it; the kept text is stored with the page it came from, with the author when the page's metadata named one, and with the site's title when the reader's subscriptions know the site the page belongs to. `mews:quotations` lists what is kept, one row per quotation naming the author, the article's title and the site it came from — by its own title when the reader's subscriptions know it, by its host otherwise — with the day it was kept, ordered by that day or by the page it came from, narrowed by a search over the text, the author, the page's title and its address. A row shares its quotation through the system share sheet, together with a citation shaped as APA names a web page: the author first when the page carried one, otherwise the article's title; the year as a no-date marker, since a page declares no date; then the article's title, the site's own title (its host when the subscriptions do not know it), and the address. A row can remove its quotation, and nothing else is offered, since the citation is the record of where the words came from.
- The reader can keep marked text as a quotation. Marking text on a page offers, in the selection toolbar, to save it, and a short note confirms the save, since closing the toolbar looks the same either way; the kept text is stored with the page it came from, with the author when the page's metadata named one, and with the site's title when the reader's subscriptions know the site the page belongs to. `mews:quotations` lists what is kept, one row per quotation naming the author, the article's title and the site it came from — by its own title when the reader's subscriptions know it, by its host otherwise — with the day it was kept, ordered by that day or by the page it came from, narrowed by a search over the text, the author, the page's title and its address. A row shares its quotation through the system share sheet, together with a citation shaped as APA names a web page: the author first when the page carried one, otherwise the article's title; the year as a no-date marker, since a page declares no date; then the article's title, the site's own title (its host when the subscriptions do not know it), and the address. A row can remove its quotation, and nothing else is offered, since the citation is the record of where the words came from.
- Each library page carries a search box that narrows it by title or address as the reader types, a quarter second after they stop. The query rides in the address as any other view state does, but it replaces the step of the walk they are standing on rather than adding one, so a search of eight letters is one step back and not eight; the box keeps what they typed as the page rebuilds under them, and follows the page when the page sets it, as clearing the search does. Submitting the box still works, for a reader who would rather say when to search. Each row carries its own actions: remove a bookmark, leave a site or check it now, forget a page. History lists one row per address, headed by the most recent visit, rather than repeating a page once per visit. Clearing history takes a second step on a page the reader can simply leave, since it cannot be undone.
- A change is a link too: following `mews:do/<verb>` applies it and rerenders the page in place without adding a history entry. Only a link followed from one of the client's own pages is honored, so a fetched page cannot clear history or change a setting on the reader's behalf (spec 8.1). A form on one of these pages is routed the same way rather than sent to the network.
- Our own pages are built from the same blocks as a page but are interface, not an article, so the renderer lays them out differently: wider before they stop growing, denser, rows divided by a line instead of bulleted, section headings quieter, a note under a row set smaller and dimmer, links carried by weight rather than an underline, and a one-field form on a single line.

View file

@ -338,12 +338,24 @@ class _ReaderScreenState extends State<ReaderScreen> {
deps.browser.narrowWhileTyping(form.action, values),
// The reader's marked text is kept with the page it came from, so
// its citation can name the author when the page did.
onSaveQuotation: (text) => deps.db.addQuotation(
quote: text,
pageUrl: page.url,
pageTitle: page.title,
author: page.author,
),
onSaveQuotation: (text) async {
// Taken before the wait, so the note does not reach across it.
final messenger = ScaffoldMessenger.of(context);
final saved = AppScope.of(context).strings.t('quotations-saved');
await deps.db.addQuotation(
quote: text,
pageUrl: page.url,
pageTitle: page.title,
author: page.author,
);
if (!mounted) return;
// Closing the toolbar looks the same either way, so the save
// says it happened.
messenger.showSnackBar(SnackBar(
duration: const Duration(seconds: 2),
content: Text(saved),
));
},
fetchImageBytes: deps.browser.fetchImageBytes,
scrollController: _scroll,
);

View file

@ -70,6 +70,8 @@ posts-mark-unread = Als ungelesen markieren
# The selection toolbar button that keeps the marked text.
quotations-save = Als Zitat speichern
# Says the marked text was kept.
quotations-saved = Als Zitat gespeichert
quotations-title = Zitate
quotations-empty = Text, den du von einer Seite behältst, erscheint hier.
quotations-no-matches = Kein Zitat passt zu deiner Suche.

View file

@ -70,6 +70,8 @@ posts-mark-unread = Mark as unread
# The selection toolbar button that keeps the marked text.
quotations-save = Save as quotation
# Says the marked text was kept.
quotations-saved = Saved as a quotation
quotations-title = Quotations
quotations-empty = Text you keep from a page appears here.
quotations-no-matches = No quotations match your search.

View file

@ -70,6 +70,8 @@ posts-mark-unread = Marcar como no leída
# The selection toolbar button that keeps the marked text.
quotations-save = Guardar como cita
# Says the marked text was kept.
quotations-saved = Guardada como cita
quotations-title = Citas
quotations-empty = El texto que guardes de una página aparece aquí.
quotations-no-matches = Ninguna cita coincide con tu búsqueda.

View file

@ -70,6 +70,8 @@ posts-mark-unread = Marquer comme non lu
# The selection toolbar button that keeps the marked text.
quotations-save = Enregistrer comme citation
# Says the marked text was kept.
quotations-saved = Enregistrée comme citation
quotations-title = Citations
quotations-empty = Le texte que vous gardez d’une page apparaît ici.
quotations-no-matches = Aucune citation ne correspond à votre recherche.

View file

@ -76,6 +76,8 @@ posts-mark-unread = סימון כלא נקראה
# The selection toolbar button that keeps the marked text.
quotations-save = שמירה כציטוט
# Says the marked text was kept.
quotations-saved = נשמר כציטוט
quotations-title = ציטוטים
quotations-empty = טקסט שתשמרו מעמוד יופיע כאן.
quotations-no-matches = אין ציטוטים שתואמים לחיפוש שלכם.

View file

@ -70,6 +70,8 @@ posts-mark-unread = 未読にする
# The selection toolbar button that keeps the marked text.
quotations-save = 引用として保存
# Says the marked text was kept.
quotations-saved = 引用として保存しました
quotations-title = 引用
quotations-empty = ページから保存したテキストがここに表示されます。
quotations-no-matches = 検索に一致する引用はありません。

View file

@ -70,6 +70,8 @@ posts-mark-unread = Marcar como não lida
# The selection toolbar button that keeps the marked text.
quotations-save = Salvar como citação
# Says the marked text was kept.
quotations-saved = Salva como citação
quotations-title = Citações
quotations-empty = O texto que você guardar de uma página aparece aqui.
quotations-no-matches = Nenhuma citação corresponde à sua busca.

View file

@ -76,6 +76,8 @@ posts-mark-unread = Отметить непрочитанной
# The selection toolbar button that keeps the marked text.
quotations-save = Сохранить как цитату
# Says the marked text was kept.
quotations-saved = Сохранено как цитата
quotations-title = Цитаты
quotations-empty = Текст, который вы сохраните со страницы, появится здесь.
quotations-no-matches = Ни одна цитата не совпадает с вашим поиском.

View file

@ -1231,6 +1231,10 @@ void main() {
await tester.pumpAndSettle();
await tester.tap(find.text('Save as quotation'));
await tester.pump();
// The save says it happened; closing the toolbar would not.
expect(find.text('Saved as a quotation'), findsOneWidget);
await tester.pumpAndSettle();
// The selection kept its page: address, title and the author the page's