feat(learn): search ranks titles first; privacy changes show progress
- The Library's search lists titles starting with what is typed first, then titles holding it as a word, then secondary titles, rather than by the shelves' recency. - The offer to make every studyset private is gone: each studyset's lock already says and changes it, and new ones are private. - Making a studyset private or not from its sheet flips the switch as soon as the person agrees and says that everything in it is being written anew, rather than standing still for a minute. - Jellyfin's password can be shown while typed. Whats-New: Searching the library lists title matches first; changing a studyset's privacy shows its progress; Jellyfin's password can be shown. Whats-New-De: Die Suche in der Bibliothek zeigt Treffer im Titel zuerst; das Ändern der Privatsphäre eines Lernsets zeigt den Fortschritt; das Jellyfin-Passwort lässt sich anzeigen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
29f900992b
commit
1cc27ef0d6
17 changed files with 276 additions and 235 deletions
File diff suppressed because one or more lines are too long
|
|
@ -2077,10 +2077,6 @@
|
||||||
"@cardMovePrivateBody": {"placeholders": {"title": {"type": "String"}}},
|
"@cardMovePrivateBody": {"placeholders": {"title": {"type": "String"}}},
|
||||||
"cardMovePrivateAction": "Privat machen und verschieben",
|
"cardMovePrivateAction": "Privat machen und verschieben",
|
||||||
"privacyKeysMissing": "Melde dich auf diesem Gerät mit deinem Passwort an: Was du hier hinzufügst oder änderst, wäre privat.",
|
"privacyKeysMissing": "Melde dich auf diesem Gerät mit deinem Passwort an: Was du hier hinzufügst oder änderst, wäre privat.",
|
||||||
"libraryOfferPrivateTitle": "Deine Lernsets privat machen?",
|
|
||||||
"libraryOfferPrivateBody": "Neue Lernsets sind privat: Ende-zu-Ende-verschlüsselt, sodass nur deine Geräte sie lesen können. Die, die du vorher gemacht hast, bleiben für die Instanz lesbar, bis du sie auch privat machst. Kopien in den Sicherungen der Instanz bleiben lesbar, bis diese ablaufen.",
|
|
||||||
"libraryOfferPrivateAction": "Alle privat machen",
|
|
||||||
"libraryOfferPrivateLater": "Jetzt nicht",
|
|
||||||
"exportReadableNote": "Das Archiv ist nicht verschlüsselt: Wer die Datei hat, kann es lesen.",
|
"exportReadableNote": "Das Archiv ist nicht verschlüsselt: Wer die Datei hat, kann es lesen.",
|
||||||
"studysetVisibilityReadable": "Nicht privat, nicht veröffentlicht",
|
"studysetVisibilityReadable": "Nicht privat, nicht veröffentlicht",
|
||||||
"studysetShareReadable": "Teilen: nicht privat",
|
"studysetShareReadable": "Teilen: nicht privat",
|
||||||
|
|
@ -2202,5 +2198,7 @@
|
||||||
"markdownToShareToday": "heute bis {percent} %",
|
"markdownToShareToday": "heute bis {percent} %",
|
||||||
"markdownDeadlinesProgress": "{furthest} %/{target} % · {when}",
|
"markdownDeadlinesProgress": "{furthest} %/{target} % · {when}",
|
||||||
"readerShowSource": "Quelltext zeigen",
|
"readerShowSource": "Quelltext zeigen",
|
||||||
"readerShowFormatted": "Formatiert zeigen"
|
"readerShowFormatted": "Formatiert zeigen",
|
||||||
|
"studysetSheetMakingPrivate": "Alles darin wird verschlüsselt … Das kann eine Minute dauern; du kannst das hier solange schließen.",
|
||||||
|
"studysetSheetMakingReadable": "Alles darin wird lesbar gemacht … Das kann eine Minute dauern; du kannst das hier solange schließen."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2490,10 +2490,6 @@
|
||||||
"@cardMovePrivateBody": {"placeholders": {"title": {"type": "String"}}},
|
"@cardMovePrivateBody": {"placeholders": {"title": {"type": "String"}}},
|
||||||
"cardMovePrivateAction": "Make private and move",
|
"cardMovePrivateAction": "Make private and move",
|
||||||
"privacyKeysMissing": "Sign in with your password on this device: what you add or change here would be private.",
|
"privacyKeysMissing": "Sign in with your password on this device: what you add or change here would be private.",
|
||||||
"libraryOfferPrivateTitle": "Make your studysets private?",
|
|
||||||
"libraryOfferPrivateBody": "New studysets are private: end-to-end encrypted, so only your devices can read them. Those you made before stay readable by the instance until you make them private too. Copies in the instance's backups stay readable until those expire.",
|
|
||||||
"libraryOfferPrivateAction": "Make all private",
|
|
||||||
"libraryOfferPrivateLater": "Not now",
|
|
||||||
"exportReadableNote": "The archive isn't encrypted: whoever has the file can read it.",
|
"exportReadableNote": "The archive isn't encrypted: whoever has the file can read it.",
|
||||||
"studysetVisibilityReadable": "Not private, not published",
|
"studysetVisibilityReadable": "Not private, not published",
|
||||||
"studysetShareReadable": "Share: not private",
|
"studysetShareReadable": "Share: not private",
|
||||||
|
|
@ -2730,5 +2726,9 @@
|
||||||
"readerShowSource": "Show the source text",
|
"readerShowSource": "Show the source text",
|
||||||
"@readerShowSource": {"description": "Tooltip: shows a Markdown file as written, rather than formatted"},
|
"@readerShowSource": {"description": "Tooltip: shows a Markdown file as written, rather than formatted"},
|
||||||
"readerShowFormatted": "Show it formatted",
|
"readerShowFormatted": "Show it formatted",
|
||||||
"@readerShowFormatted": {"description": "Tooltip: shows a Markdown file formatted again, after its source text"}
|
"@readerShowFormatted": {"description": "Tooltip: shows a Markdown file formatted again, after its source text"},
|
||||||
|
"studysetSheetMakingPrivate": "Encrypting everything in it… This can take a minute; you can close this meanwhile.",
|
||||||
|
"@studysetSheetMakingPrivate": {"description": "Below the Private switch of a studyset's sheet while it is being made private"},
|
||||||
|
"studysetSheetMakingReadable": "Making everything in it readable… This can take a minute; you can close this meanwhile.",
|
||||||
|
"@studysetSheetMakingReadable": {"description": "Below the Private switch of a studyset's sheet while it is being made readable by the instance"}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12242,30 +12242,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Sign in with your password on this device: what you add or change here would be private.'**
|
/// **'Sign in with your password on this device: what you add or change here would be private.'**
|
||||||
String get privacyKeysMissing;
|
String get privacyKeysMissing;
|
||||||
|
|
||||||
/// No description provided for @libraryOfferPrivateTitle.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Make your studysets private?'**
|
|
||||||
String get libraryOfferPrivateTitle;
|
|
||||||
|
|
||||||
/// No description provided for @libraryOfferPrivateBody.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'New studysets are private: end-to-end encrypted, so only your devices can read them. Those you made before stay readable by the instance until you make them private too. Copies in the instance\'s backups stay readable until those expire.'**
|
|
||||||
String get libraryOfferPrivateBody;
|
|
||||||
|
|
||||||
/// No description provided for @libraryOfferPrivateAction.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Make all private'**
|
|
||||||
String get libraryOfferPrivateAction;
|
|
||||||
|
|
||||||
/// No description provided for @libraryOfferPrivateLater.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Not now'**
|
|
||||||
String get libraryOfferPrivateLater;
|
|
||||||
|
|
||||||
/// No description provided for @exportReadableNote.
|
/// No description provided for @exportReadableNote.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -12997,6 +12973,18 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Show it formatted'**
|
/// **'Show it formatted'**
|
||||||
String get readerShowFormatted;
|
String get readerShowFormatted;
|
||||||
|
|
||||||
|
/// Below the Private switch of a studyset's sheet while it is being made private
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Encrypting everything in it… This can take a minute; you can close this meanwhile.'**
|
||||||
|
String get studysetSheetMakingPrivate;
|
||||||
|
|
||||||
|
/// Below the Private switch of a studyset's sheet while it is being made readable by the instance
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Making everything in it readable… This can take a minute; you can close this meanwhile.'**
|
||||||
|
String get studysetSheetMakingReadable;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -7888,19 +7888,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get privacyKeysMissing =>
|
String get privacyKeysMissing =>
|
||||||
'Melde dich auf diesem Gerät mit deinem Passwort an: Was du hier hinzufügst oder änderst, wäre privat.';
|
'Melde dich auf diesem Gerät mit deinem Passwort an: Was du hier hinzufügst oder änderst, wäre privat.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get libraryOfferPrivateTitle => 'Deine Lernsets privat machen?';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get libraryOfferPrivateBody =>
|
|
||||||
'Neue Lernsets sind privat: Ende-zu-Ende-verschlüsselt, sodass nur deine Geräte sie lesen können. Die, die du vorher gemacht hast, bleiben für die Instanz lesbar, bis du sie auch privat machst. Kopien in den Sicherungen der Instanz bleiben lesbar, bis diese ablaufen.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get libraryOfferPrivateAction => 'Alle privat machen';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get libraryOfferPrivateLater => 'Jetzt nicht';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get exportReadableNote =>
|
String get exportReadableNote =>
|
||||||
'Das Archiv ist nicht verschlüsselt: Wer die Datei hat, kann es lesen.';
|
'Das Archiv ist nicht verschlüsselt: Wer die Datei hat, kann es lesen.';
|
||||||
|
|
@ -8323,4 +8310,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get readerShowFormatted => 'Formatiert zeigen';
|
String get readerShowFormatted => 'Formatiert zeigen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get studysetSheetMakingPrivate =>
|
||||||
|
'Alles darin wird verschlüsselt … Das kann eine Minute dauern; du kannst das hier solange schließen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get studysetSheetMakingReadable =>
|
||||||
|
'Alles darin wird lesbar gemacht … Das kann eine Minute dauern; du kannst das hier solange schließen.';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7816,19 +7816,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get privacyKeysMissing =>
|
String get privacyKeysMissing =>
|
||||||
'Sign in with your password on this device: what you add or change here would be private.';
|
'Sign in with your password on this device: what you add or change here would be private.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get libraryOfferPrivateTitle => 'Make your studysets private?';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get libraryOfferPrivateBody =>
|
|
||||||
'New studysets are private: end-to-end encrypted, so only your devices can read them. Those you made before stay readable by the instance until you make them private too. Copies in the instance\'s backups stay readable until those expire.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get libraryOfferPrivateAction => 'Make all private';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get libraryOfferPrivateLater => 'Not now';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get exportReadableNote =>
|
String get exportReadableNote =>
|
||||||
'The archive isn\'t encrypted: whoever has the file can read it.';
|
'The archive isn\'t encrypted: whoever has the file can read it.';
|
||||||
|
|
@ -8249,6 +8236,14 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get readerShowFormatted => 'Show it formatted';
|
String get readerShowFormatted => 'Show it formatted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get studysetSheetMakingPrivate =>
|
||||||
|
'Encrypting everything in it… This can take a minute; you can close this meanwhile.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get studysetSheetMakingReadable =>
|
||||||
|
'Making everything in it readable… This can take a minute; you can close this meanwhile.';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,6 @@ import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_reposit
|
||||||
import 'package:halkyon_learn_flutter/src/feature/settings/data/sync_settings_gateway.dart';
|
import 'package:halkyon_learn_flutter/src/feature/settings/data/sync_settings_gateway.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/study/data/sync_review_gateway.dart';
|
import 'package:halkyon_learn_flutter/src/feature/study/data/sync_review_gateway.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/private_offer_store.dart';
|
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_progress_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_progress_repository.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
|
||||||
|
|
@ -635,9 +634,6 @@ Future<PersonDependencies> _openPersonData(
|
||||||
File(p.join(folder.path, 'pomodoro.json')),
|
File(p.join(folder.path, 'pomodoro.json')),
|
||||||
),
|
),
|
||||||
studysetTabs: studysetTabs,
|
studysetTabs: studysetTabs,
|
||||||
privateOffer: FilePrivateOfferStore(
|
|
||||||
File(p.join(folder.path, 'private_offer')),
|
|
||||||
),
|
|
||||||
publishing: publishing,
|
publishing: publishing,
|
||||||
explore: ExploreRepository(
|
explore: ExploreRepository(
|
||||||
ServerpodExploreGateway(client),
|
ServerpodExploreGateway(client),
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/pomodoro_run_sto
|
||||||
import 'package:halkyon_learn_flutter/src/feature/publish/data/publish_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/publish/data/publish_repository.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/private_offer_store.dart';
|
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_progress_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_progress_repository.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
|
||||||
|
|
@ -106,10 +105,6 @@ class PersonDependencies({
|
||||||
/// The tab each of the person's studysets was left on, on this device.
|
/// The tab each of the person's studysets was left on, on this device.
|
||||||
required final StudysetTabs studysetTabs,
|
required final StudysetTabs studysetTabs,
|
||||||
|
|
||||||
/// Whether the library's offer to make every studyset private was
|
|
||||||
/// answered on this device (ADR 0019, D4).
|
|
||||||
required final PrivateOfferStore privateOffer,
|
|
||||||
|
|
||||||
/// Publishing the person's studysets, on the instance.
|
/// Publishing the person's studysets, on the instance.
|
||||||
required final PublishRepository publishing,
|
required final PublishRepository publishing,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:halkyon_app/halkyon_app.dart';
|
||||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies.dart';
|
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_gateway.dart';
|
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_gateway.dart';
|
||||||
|
|
@ -40,6 +41,9 @@ class _JellyfinConnectFormState() extends State<JellyfinConnectForm> {
|
||||||
final _address = TextEditingController();
|
final _address = TextEditingController();
|
||||||
final _user = TextEditingController();
|
final _user = TextEditingController();
|
||||||
final _password = TextEditingController();
|
final _password = TextEditingController();
|
||||||
|
|
||||||
|
/// Whether the password is shown as typed.
|
||||||
|
var _passwordShown = false;
|
||||||
final _passwordFocus = FocusNode();
|
final _passwordFocus = FocusNode();
|
||||||
JellyfinServer? _server;
|
JellyfinServer? _server;
|
||||||
List<JellyfinPublicUser> _publicUsers = const [];
|
List<JellyfinPublicUser> _publicUsers = const [];
|
||||||
|
|
@ -289,13 +293,25 @@ class _JellyfinConnectFormState() extends State<JellyfinConnectForm> {
|
||||||
focusNode: _passwordFocus,
|
focusNode: _passwordFocus,
|
||||||
maxLength: TextLimits.phrase,
|
maxLength: TextLimits.phrase,
|
||||||
buildCounter: nearLimitCounter,
|
buildCounter: nearLimitCounter,
|
||||||
obscureText: true,
|
obscureText: !_passwordShown,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
textInputAction: TextInputAction.go,
|
textInputAction: TextInputAction.go,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: l10n.jellyfinPassword,
|
labelText: l10n.jellyfinPassword,
|
||||||
helperText: l10n.jellyfinPasswordNone,
|
helperText: l10n.jellyfinPasswordNone,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
tooltip: _passwordShown
|
||||||
|
? HalkyonAppLocalizations.of(context).passwordHide
|
||||||
|
: HalkyonAppLocalizations.of(context).passwordShow,
|
||||||
|
icon: Icon(
|
||||||
|
_passwordShown ? LucideIcons.eyeOff : LucideIcons.eye,
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
setState(() => _passwordShown = !_passwordShown),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onSubmitted: _busy ? null : (_) => _signIn(),
|
onSubmitted: _busy ? null : (_) => _signIn(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import 'package:halkyon_learn_flutter/src/feature/study/widget/start_studying.da
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/bloc/studyset_list_bloc.dart';
|
import 'package:halkyon_learn_flutter/src/feature/studyset/bloc/studyset_list_bloc.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/library_shelves.dart';
|
import 'package:halkyon_learn_flutter/src/feature/studyset/model/library_shelves.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
|
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_tree.dart';
|
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/subject.dart';
|
import 'package:halkyon_learn_flutter/src/feature/studyset/model/subject.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/delete_studyset.dart';
|
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/delete_studyset.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/deleted_studyset_card.dart';
|
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/deleted_studyset_card.dart';
|
||||||
|
|
@ -507,8 +506,35 @@ class _GridState() extends State<_Grid> {
|
||||||
);
|
);
|
||||||
|
|
||||||
static bool _matches(Studyset studyset, String query) =>
|
static bool _matches(Studyset studyset, String query) =>
|
||||||
studyset.content.title.toLowerCase().contains(query) ||
|
_rankOf(studyset, query) != null;
|
||||||
(studyset.content.secondaryTitle?.toLowerCase().contains(query) ?? false);
|
|
||||||
|
/// How well [studyset] matches [query], lower first: its title starting
|
||||||
|
/// with it, a word of its title starting with it, its title holding it,
|
||||||
|
/// then its secondary title alike; null for no match.
|
||||||
|
static int? _rankOf(Studyset studyset, String query) {
|
||||||
|
int? rankIn(String? text) {
|
||||||
|
final lower = text?.toLowerCase();
|
||||||
|
if (lower == null || !lower.contains(query)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (lower.startsWith(query)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return lower.contains(
|
||||||
|
RegExp(r'(^|[^\p{L}\p{N}])' + RegExp.escape(query), unicode: true),
|
||||||
|
)
|
||||||
|
? 1
|
||||||
|
: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rankIn(studyset.content.title) case final rank?) {
|
||||||
|
return rank;
|
||||||
|
}
|
||||||
|
if (rankIn(studyset.content.secondaryTitle) case final rank?) {
|
||||||
|
return 3 + rank;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// The studysets out on Explore, as last known.
|
/// The studysets out on Explore, as last known.
|
||||||
var _published = const <String>{};
|
var _published = const <String>{};
|
||||||
|
|
@ -540,6 +566,26 @@ class _GridState() extends State<_Grid> {
|
||||||
progress: widget.progress,
|
progress: widget.progress,
|
||||||
now: clock.now().toUtc(),
|
now: clock.now().toUtc(),
|
||||||
);
|
);
|
||||||
|
// A search lists its matches by how well they match, in one grid, the
|
||||||
|
// shelves' order within the same rank.
|
||||||
|
final ranked = searching
|
||||||
|
? ([
|
||||||
|
for (final (index, studyset) in [
|
||||||
|
for (final listed in shelves.values) ...listed,
|
||||||
|
].indexed)
|
||||||
|
(
|
||||||
|
rank: _rankOf(studyset, query)!,
|
||||||
|
index: index,
|
||||||
|
studyset: studyset,
|
||||||
|
),
|
||||||
|
]..sort(
|
||||||
|
(a, b) => a.rank != b.rank
|
||||||
|
? a.rank.compareTo(b.rank)
|
||||||
|
: a.index.compareTo(b.index),
|
||||||
|
))
|
||||||
|
.map((match) => match.studyset)
|
||||||
|
.toList()
|
||||||
|
: const <Studyset>[];
|
||||||
final archived = [
|
final archived = [
|
||||||
for (final studyset in widget.studysets)
|
for (final studyset in widget.studysets)
|
||||||
if (studyset.isArchived && listed(studyset)) studyset,
|
if (studyset.isArchived && listed(studyset)) studyset,
|
||||||
|
|
@ -567,13 +613,24 @@ class _GridState() extends State<_Grid> {
|
||||||
hasScrollBody: false,
|
hasScrollBody: false,
|
||||||
child: _Message(l10n.librarySearchNoMatches),
|
child: _Message(l10n.librarySearchNoMatches),
|
||||||
),
|
),
|
||||||
if (!searching)
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: _PrivateOffer(studysets: widget.studysets),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: _SubjectDropdown(subjects: inUse, selected: subject),
|
child: _SubjectDropdown(subjects: inUse, selected: subject),
|
||||||
),
|
),
|
||||||
|
if (searching && ranked.isNotEmpty)
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(
|
||||||
|
_spacing,
|
||||||
|
_spacing,
|
||||||
|
_spacing,
|
||||||
|
_spacing,
|
||||||
|
),
|
||||||
|
sliver: SliverGrid.builder(
|
||||||
|
gridDelegate: _layout,
|
||||||
|
itemCount: ranked.length,
|
||||||
|
itemBuilder: (context, index) => _card(context, ranked[index]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!searching)
|
||||||
for (final MapEntry(key: shelf, value: listed)
|
for (final MapEntry(key: shelf, value: listed)
|
||||||
in shelves.entries) ...[
|
in shelves.entries) ...[
|
||||||
SliverToBoxAdapter(child: _ShelfHeading(shelf)),
|
SliverToBoxAdapter(child: _ShelfHeading(shelf)),
|
||||||
|
|
@ -587,7 +644,8 @@ class _GridState() extends State<_Grid> {
|
||||||
sliver: SliverGrid.builder(
|
sliver: SliverGrid.builder(
|
||||||
gridDelegate: _layout,
|
gridDelegate: _layout,
|
||||||
itemCount: listed.length,
|
itemCount: listed.length,
|
||||||
itemBuilder: (context, index) => _card(context, listed[index]),
|
itemBuilder: (context, index) =>
|
||||||
|
_card(context, listed[index]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -742,142 +800,6 @@ class _GridState() extends State<_Grid> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What Recently deleted keeps, and for how long.
|
/// What Recently deleted keeps, and for how long.
|
||||||
/// Offers once, on this device, to make every studyset that isn't private
|
|
||||||
/// private (ADR 0019, D4): new ones are already, and those from before
|
|
||||||
/// stay readable until the person chooses. Shown only where this device
|
|
||||||
/// can encrypt and some studyset isn't private; gone once answered.
|
|
||||||
class const _PrivateOffer({required final List<Studyset> studysets})
|
|
||||||
extends StatefulWidget {
|
|
||||||
@override
|
|
||||||
State<_PrivateOffer> createState() => _PrivateOfferState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PrivateOfferState() extends State<_PrivateOffer> {
|
|
||||||
/// Whether it was answered; null until read.
|
|
||||||
bool? _answered;
|
|
||||||
var _busy = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
unawaited(() async {
|
|
||||||
final person = PersonDependencies.of(context);
|
|
||||||
// Answered on this device, or on another of the person's (the
|
|
||||||
// reading of 22 Oct: the answer follows them).
|
|
||||||
final answered =
|
|
||||||
await person.privateOffer.answered() ||
|
|
||||||
(await person.settings.read()).privateOfferAnswered;
|
|
||||||
if (mounted) {
|
|
||||||
setState(() => _answered = answered);
|
|
||||||
}
|
|
||||||
}());
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Studyset> get _readable => [
|
|
||||||
for (final studyset in widget.studysets)
|
|
||||||
if (studyset.parentId == null &&
|
|
||||||
!studyset.isPrivate &&
|
|
||||||
!studyset.isLocked)
|
|
||||||
studyset,
|
|
||||||
];
|
|
||||||
|
|
||||||
Future<void> _answer({required bool makePrivate}) async {
|
|
||||||
final person = PersonDependencies.of(context);
|
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
|
||||||
final l10n = AppLocalizations.of(context);
|
|
||||||
setState(() => _busy = true);
|
|
||||||
try {
|
|
||||||
if (makePrivate) {
|
|
||||||
// Every top-level studyset, those waiting in Recently deleted and
|
|
||||||
// those whose parent is gone among them (the reading of 22 Oct).
|
|
||||||
var left = 0;
|
|
||||||
for (final studyset in topLevel(await person.studysets.withDeleted())) {
|
|
||||||
if (!studyset.isPrivate && !studyset.isLocked) {
|
|
||||||
left += await person.studysets.setPrivate(
|
|
||||||
studyset.id,
|
|
||||||
private: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (left > 0) {
|
|
||||||
messenger.showSnackBar(
|
|
||||||
SnackBar(content: Text(l10n.studysetPrivateSomeLocked)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await person.privateOffer.answer();
|
|
||||||
await person.settings.update(
|
|
||||||
(settings) => settings.copyWith(privateOfferAnswered: true),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_busy = false;
|
|
||||||
_answered = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final person = PersonDependencies.of(context);
|
|
||||||
if (_answered != false ||
|
|
||||||
!person.studysets.canMakePrivate ||
|
|
||||||
_readable.isEmpty) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
final l10n = AppLocalizations.of(context);
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
|
|
||||||
child: Card(
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(LucideIcons.lock),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
l10n.libraryOfferPrivateTitle,
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(l10n.libraryOfferPrivateBody),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 8,
|
|
||||||
alignment: WrapAlignment.end,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: _busy ? null : () => _answer(makePrivate: false),
|
|
||||||
child: Text(l10n.libraryOfferPrivateLater),
|
|
||||||
),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: _busy ? null : () => _answer(makePrivate: true),
|
|
||||||
child: _busy
|
|
||||||
? const ButtonLoadingIndicator()
|
|
||||||
: Text(l10n.libraryOfferPrivateAction),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class const _DeletedNote() extends StatelessWidget {
|
class const _DeletedNote() extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
||||||
|
|
@ -275,7 +275,12 @@ Future<void> editStudyset(
|
||||||
// at once, once the person agreed.
|
// at once, once the person agreed.
|
||||||
private: parentId == null ? studyset.isPrivate : null,
|
private: parentId == null ? studyset.isPrivate : null,
|
||||||
onPrivate: parentId == null
|
onPrivate: parentId == null
|
||||||
? (private) => setStudysetPrivacy(context, studyset, private: private)
|
? (private, agreed) => setStudysetPrivacy(
|
||||||
|
context,
|
||||||
|
studyset,
|
||||||
|
private: private,
|
||||||
|
onAgreed: agreed,
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
insideAnother: parentId != null,
|
insideAnother: parentId != null,
|
||||||
inheritedColorName: parentId == null ? null : colorNameOf(parentId, all),
|
inheritedColorName: parentId == null ? null : colorNameOf(parentId, all),
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,13 @@ import 'package:material_ui/material_ui.dart';
|
||||||
/// readable by the instance again (ADR 0019), once the person agreed: the
|
/// readable by the instance again (ADR 0019), once the person agreed: the
|
||||||
/// dialog says what changes, and that the instance's backups keep readable
|
/// dialog says what changes, and that the instance's backups keep readable
|
||||||
/// copies until they expire. Where this device can't encrypt (an account
|
/// copies until they expire. Where this device can't encrypt (an account
|
||||||
/// whose keys aren't here), says how to. Whether it was changed.
|
/// whose keys aren't here), says how to. Calls [onAgreed] as the person
|
||||||
|
/// agrees, before the rewrite, which takes a while. Whether it was changed.
|
||||||
Future<bool> setStudysetPrivacy(
|
Future<bool> setStudysetPrivacy(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Studyset studyset, {
|
Studyset studyset, {
|
||||||
required bool private,
|
required bool private,
|
||||||
|
VoidCallback? onAgreed,
|
||||||
}) async {
|
}) async {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
if (private && !PersonDependencies.of(context).studysets.canMakePrivate) {
|
if (private && !PersonDependencies.of(context).studysets.canMakePrivate) {
|
||||||
|
|
@ -54,6 +56,7 @@ Future<bool> setStudysetPrivacy(
|
||||||
if (confirmed != true || !context.mounted) {
|
if (confirmed != true || !context.mounted) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
onAgreed?.call();
|
||||||
await applyStudysetPrivacy(context, studyset, private: private);
|
await applyStudysetPrivacy(context, studyset, private: private);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,9 @@ Future<StudysetContent?> showStudysetSheet(
|
||||||
bool? private,
|
bool? private,
|
||||||
|
|
||||||
/// Makes it private or not at once, once the person agreed, rather than
|
/// Makes it private or not at once, once the person agreed, rather than
|
||||||
/// on Save, which the content alone waits for; whether it was changed.
|
/// on Save, which the content alone waits for, calling `agreed` as the
|
||||||
Future<bool> Function(bool private)? onPrivate,
|
/// person agrees; whether it was changed.
|
||||||
|
Future<bool> Function(bool private, VoidCallback agreed)? onPrivate,
|
||||||
}) => showAdaptiveSheet<StudysetContent>(
|
}) => showAdaptiveSheet<StudysetContent>(
|
||||||
context,
|
context,
|
||||||
// Swiping would discard changes without asking.
|
// Swiping would discard changes without asking.
|
||||||
|
|
@ -73,7 +74,7 @@ class const _StudysetSheet({
|
||||||
required final StudysetLicense inheritedLicense,
|
required final StudysetLicense inheritedLicense,
|
||||||
final String? studysetId,
|
final String? studysetId,
|
||||||
final bool? private,
|
final bool? private,
|
||||||
final Future<bool> Function(bool private)? onPrivate,
|
final Future<bool> Function(bool private, VoidCallback agreed)? onPrivate,
|
||||||
}) extends StatefulWidget {
|
}) extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
State<_StudysetSheet> createState() => _StudysetSheetState();
|
State<_StudysetSheet> createState() => _StudysetSheetState();
|
||||||
|
|
@ -129,13 +130,40 @@ class _StudysetSheetState() extends State<_StudysetSheet> {
|
||||||
late String? _frontLanguage = widget.initial?.frontLanguage;
|
late String? _frontLanguage = widget.initial?.frontLanguage;
|
||||||
late String? _backLanguage = widget.initial?.backLanguage;
|
late String? _backLanguage = widget.initial?.backLanguage;
|
||||||
|
|
||||||
/// Whether the studyset is private now, as last changed here.
|
/// Whether the studyset is private now, as last changed here: as soon
|
||||||
|
/// as the person agreed, while everything in it is written anew.
|
||||||
late var _private = widget.private;
|
late var _private = widget.private;
|
||||||
|
|
||||||
|
/// Whether its privacy is being changed: every row of its tree written
|
||||||
|
/// anew, a minute for thousands of cards (the developer's review, 10 Nov
|
||||||
|
/// 2026: the switch stood still meanwhile, as if nothing happened).
|
||||||
|
var _changing = false;
|
||||||
|
|
||||||
Future<void> _setPrivate(bool private) async {
|
Future<void> _setPrivate(bool private) async {
|
||||||
if (await widget.onPrivate?.call(private) ?? false) {
|
final before = _private;
|
||||||
|
try {
|
||||||
|
final changed =
|
||||||
|
await widget.onPrivate?.call(private, () {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _private = private);
|
setState(() {
|
||||||
|
_private = private;
|
||||||
|
_changing = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}) ??
|
||||||
|
false;
|
||||||
|
if (mounted && !changed) {
|
||||||
|
setState(() => _private = before);
|
||||||
|
}
|
||||||
|
} on Object {
|
||||||
|
// Cut short, as it stood: the rewrite said why.
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _private = before);
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _changing = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -573,7 +601,22 @@ class _StudysetSheetState() extends State<_StudysetSheet> {
|
||||||
),
|
),
|
||||||
title: Text(l10n.studysetSheetPrivate),
|
title: Text(l10n.studysetSheetPrivate),
|
||||||
value: private,
|
value: private,
|
||||||
onChanged: _setPrivate,
|
onChanged: _changing ? null : _setPrivate,
|
||||||
|
),
|
||||||
|
if (_changing)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const ButtonLoadingIndicator(),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
private
|
||||||
|
? l10n.studysetSheetMakingPrivate
|
||||||
|
: l10n.studysetSheetMakingReadable,
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,16 @@ void main() {
|
||||||
expect(find.text('Living room'), findsOneWidget);
|
expect(find.text('Living room'), findsOneWidget);
|
||||||
await tester.enterText(find.widgetWithText(TextField, 'User name'), 'mika');
|
await tester.enterText(find.widgetWithText(TextField, 'User name'), 'mika');
|
||||||
await tester.enterText(find.widgetWithText(TextField, 'Password'), 'wrong');
|
await tester.enterText(find.widgetWithText(TextField, 'Password'), 'wrong');
|
||||||
|
bool hidden() => tester
|
||||||
|
.widget<TextField>(find.widgetWithText(TextField, 'Password'))
|
||||||
|
.obscureText;
|
||||||
|
expect(hidden(), isTrue);
|
||||||
|
await tester.tap(find.byTooltip('Show password'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(hidden(), isFalse);
|
||||||
|
await tester.tap(find.byTooltip('Hide password'));
|
||||||
|
await tester.pump();
|
||||||
|
expect(hidden(), isTrue);
|
||||||
await tester.tap(find.text('Sign in'));
|
await tester.tap(find.text('Sign in'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('The user name or password is wrong.'), findsOneWidget);
|
expect(find.text('The user name or password is wrong.'), findsOneWidget);
|
||||||
|
|
|
||||||
|
|
@ -683,6 +683,44 @@ void main() {
|
||||||
expect(find.text('Kanji'), findsNothing, reason: 'the archive is closed');
|
expect(find.text('Kanji'), findsNothing, reason: 'the archive is closed');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('a search lists titles starting with what is typed first, then '
|
||||||
|
'those holding it as a word, then secondary titles, whatever was changed '
|
||||||
|
'last', (tester) async {
|
||||||
|
stored(
|
||||||
|
'tooling',
|
||||||
|
'Rust',
|
||||||
|
secondaryTitle: 'Python tooling, from the docs',
|
||||||
|
updatedAt: DateTime.utc(2026, 11, 9),
|
||||||
|
);
|
||||||
|
stored(
|
||||||
|
'learning',
|
||||||
|
'Learning Python',
|
||||||
|
updatedAt: DateTime.utc(2026, 11, 8),
|
||||||
|
);
|
||||||
|
stored('python', 'Python', updatedAt: DateTime.utc(2026, 1, 2));
|
||||||
|
await showLibrary(tester);
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Search'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.enterText(find.byType(TextField), 'python');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// In reading order: row by row, each from the start.
|
||||||
|
final places = [
|
||||||
|
for (final title in ['Python', 'Learning Python', 'Rust'])
|
||||||
|
tester.getTopLeft(find.text(title)),
|
||||||
|
];
|
||||||
|
for (final (before, after) in [
|
||||||
|
(places[0], places[1]),
|
||||||
|
(places[1], places[2]),
|
||||||
|
]) {
|
||||||
|
expect(
|
||||||
|
before.dy < after.dy || (before.dy == after.dy && before.dx < after.dx),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('says when no studyset matches the search', (tester) async {
|
testWidgets('says when no studyset matches the search', (tester) async {
|
||||||
stored('japanese', 'Japanese');
|
stored('japanese', 'Japanese');
|
||||||
await showLibrary(tester);
|
await showLibrary(tester);
|
||||||
|
|
@ -748,11 +786,45 @@ void main() {
|
||||||
expect(gateway.privacyChanges, [('japanese', true)]);
|
expect(gateway.privacyChanges, [('japanese', true)]);
|
||||||
expect(tester.widget<SwitchListTile>(private).value, isTrue);
|
expect(tester.widget<SwitchListTile>(private).value, isTrue);
|
||||||
|
|
||||||
|
// Made readable again while that takes a while: the switch follows at
|
||||||
|
// once, says what goes on, and takes no other change meanwhile.
|
||||||
|
final hold = gateway.holdPrivacy = Completer<void>();
|
||||||
|
await tester.tap(private);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.widgetWithText(FilledButton, 'Make non-private'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump();
|
||||||
|
expect(tester.widget<SwitchListTile>(private).value, isFalse);
|
||||||
|
expect(tester.widget<SwitchListTile>(private).onChanged, isNull);
|
||||||
|
expect(
|
||||||
|
find.textContaining('Making everything in it readable'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
hold.complete();
|
||||||
|
gateway.holdPrivacy = null;
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(gateway.privacyChanges, [('japanese', true), ('japanese', false)]);
|
||||||
|
expect(
|
||||||
|
find.textContaining('Making everything in it readable'),
|
||||||
|
findsNothing,
|
||||||
|
);
|
||||||
|
expect(tester.widget<SwitchListTile>(private).value, isFalse);
|
||||||
|
|
||||||
|
await tester.tap(private);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.widgetWithText(FilledButton, 'Make private'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tester.widget<SwitchListTile>(private).value, isTrue);
|
||||||
|
|
||||||
await tester.tap(private);
|
await tester.tap(private);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.text('Cancel'));
|
await tester.tap(find.text('Cancel'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(gateway.privacyChanges, [('japanese', true)], reason: 'cancelled');
|
expect(gateway.privacyChanges, [
|
||||||
|
('japanese', true),
|
||||||
|
('japanese', false),
|
||||||
|
('japanese', true),
|
||||||
|
], reason: 'cancelled');
|
||||||
expect(tester.widget<SwitchListTile>(private).value, isTrue);
|
expect(tester.widget<SwitchListTile>(private).value, isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,10 @@ class FakeStudysetGateway() implements StudysetGateway {
|
||||||
/// Every change of privacy, in order, as (id, private).
|
/// Every change of privacy, in order, as (id, private).
|
||||||
final privacyChanges = <(String, bool)>[];
|
final privacyChanges = <(String, bool)>[];
|
||||||
|
|
||||||
|
/// While set, making a tree private or readable waits for it, as a
|
||||||
|
/// rewrite of thousands of rows does.
|
||||||
|
Completer<void>? holdPrivacy;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool canMakePrivate = false;
|
bool canMakePrivate = false;
|
||||||
|
|
||||||
|
|
@ -100,6 +104,9 @@ class FakeStudysetGateway() implements StudysetGateway {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> setPrivate(String id, {required bool private}) async {
|
Future<int> setPrivate(String id, {required bool private}) async {
|
||||||
|
if (holdPrivacy case final hold?) {
|
||||||
|
await hold.future;
|
||||||
|
}
|
||||||
final tree = {id};
|
final tree = {id};
|
||||||
var grew = true;
|
var grew = true;
|
||||||
while (grew) {
|
while (grew) {
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,6 @@ import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_gateway
|
||||||
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/study/data/review_gateway.dart';
|
import 'package:halkyon_learn_flutter/src/feature/study/data/review_gateway.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/private_offer_store.dart';
|
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_gateway.dart';
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_gateway.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_progress_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_progress_repository.dart';
|
||||||
|
|
@ -109,7 +108,6 @@ import 'fake_youtube_videos.dart';
|
||||||
import 'memory_asset_file_cache.dart';
|
import 'memory_asset_file_cache.dart';
|
||||||
import 'memory_device_settings_store.dart';
|
import 'memory_device_settings_store.dart';
|
||||||
import 'memory_pomodoro_run_store.dart';
|
import 'memory_pomodoro_run_store.dart';
|
||||||
import 'memory_private_offer_store.dart';
|
|
||||||
import 'memory_published_store.dart';
|
import 'memory_published_store.dart';
|
||||||
import 'memory_studyset_tab_store.dart';
|
import 'memory_studyset_tab_store.dart';
|
||||||
|
|
||||||
|
|
@ -163,7 +161,6 @@ Dependencies testDependencies({
|
||||||
PublishGateway? publishGateway,
|
PublishGateway? publishGateway,
|
||||||
PublishedStore? publishedStore,
|
PublishedStore? publishedStore,
|
||||||
StudysetTabStore? studysetTabStore,
|
StudysetTabStore? studysetTabStore,
|
||||||
PrivateOfferStore? privateOffer,
|
|
||||||
ExploreGateway? exploreGateway,
|
ExploreGateway? exploreGateway,
|
||||||
FeedbackWriteGateway? feedbackWriteGateway,
|
FeedbackWriteGateway? feedbackWriteGateway,
|
||||||
FeedbackServerGateway? feedbackServerGateway,
|
FeedbackServerGateway? feedbackServerGateway,
|
||||||
|
|
@ -382,7 +379,6 @@ Dependencies testDependencies({
|
||||||
conversationDeletions: conversationDeletions,
|
conversationDeletions: conversationDeletions,
|
||||||
pomodoroRuns: pomodoroRuns ?? MemoryPomodoroRunStore(),
|
pomodoroRuns: pomodoroRuns ?? MemoryPomodoroRunStore(),
|
||||||
studysetTabs: studysetTabs,
|
studysetTabs: studysetTabs,
|
||||||
privateOffer: privateOffer ?? MemoryPrivateOfferStore(),
|
|
||||||
publishing: PublishRepository(
|
publishing: PublishRepository(
|
||||||
publishGateway ?? FakePublishGateway(),
|
publishGateway ?? FakePublishGateway(),
|
||||||
publishedStore ?? MemoryPublishedStore(),
|
publishedStore ?? MemoryPublishedStore(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue