feat(meditate): the instance's library in the app: Explore, its player, courses, keeping offline
Explore shows the instance's library below Meditate's practices, where it has one: its courses and items by category (Meditate's eleven, named in English and German, and the operator's own) and by the same search; an instance without one shows nothing more. An item's page: its recordings to choose, Play, a favourite (kept in the sealed settings under library/<id>), keeping it on the device and removing it, and for a sound, a silent sitting with it looped; its credit and licence. The player plays, pauses and seeks; played to its end it is kept as a sitting, and as its course's lesson done (lesson_done, sealed and synced). Settings lists the library's credits beside About; Today shows its favourites. The last catalogue is kept in the person's folder for offline; files are fetched with the catalogue's access, asked for again shortly before it ends. ADR 0038, D16. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
0b6f27d278
commit
811182b376
1 changed files with 3386 additions and 22 deletions
|
|
@ -7,6 +7,8 @@ import 'package:halkyon_meditate_flutter/src/app/meditate_dependencies.dart';
|
|||
import 'package:halkyon_meditate_flutter/src/core/privacy/meditate_keys.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/journal/data/dream_repository.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/journal/model/dream.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/library/data/lesson_repository.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/library/data/library_repository.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/settings/data/settings_repository.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/settings/model/meditate_settings.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/sitting/data/sitting_repository.dart';
|
||||
|
|
@ -14,8 +16,10 @@ import 'package:halkyon_meditate_flutter/src/feature/sitting/model/sitting.dart'
|
|||
import 'package:halkyon_meditate_flutter/src/feature/sitting/widget/sitting_screen.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/tabs/journal_tab.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../support/fake_library.dart';
|
||||
import '../support/memory_content_key_gateway.dart';
|
||||
import '../support/memory_sealed_row_gateway.dart';
|
||||
import '../support/recording_device.dart';
|
||||
|
|
@ -32,6 +36,10 @@ void main() {
|
|||
late MemorySealedRowGateway sittingRows;
|
||||
late MemorySealedRowGateway dreamRows;
|
||||
late FakeSpeechInput speech;
|
||||
late FakeLibraryGateway library;
|
||||
late MemoryLibraryDownloads downloads;
|
||||
late MemorySealedRowGateway lessonRows;
|
||||
late RecordingGuidePlayer guide;
|
||||
late RecordingBells bells;
|
||||
late RecordingScreen screen;
|
||||
|
||||
|
|
@ -56,6 +64,10 @@ void main() {
|
|||
sittingRows = MemorySealedRowGateway();
|
||||
dreamRows = MemorySealedRowGateway();
|
||||
speech = FakeSpeechInput();
|
||||
library = FakeLibraryGateway();
|
||||
downloads = MemoryLibraryDownloads();
|
||||
lessonRows = MemorySealedRowGateway();
|
||||
guide = RecordingGuidePlayer();
|
||||
bells = RecordingBells();
|
||||
screen = RecordingScreen();
|
||||
store = MemoryInstanceStore();
|
||||
|
|
@ -150,9 +162,16 @@ void main() {
|
|||
settings: settings(),
|
||||
sittings: sittings(),
|
||||
dreams: dreams(),
|
||||
library: LibraryRepository(
|
||||
library,
|
||||
store: MemoryCatalogueStore(),
|
||||
downloads: downloads,
|
||||
),
|
||||
lessons: LessonRepository(lessonRows, keys),
|
||||
bells: bells,
|
||||
screen: screen,
|
||||
speech: speech,
|
||||
guide: guide,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
@ -1141,4 +1160,215 @@ void main() {
|
|||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
group("the instance's library (ADR 0038, D16)", () {
|
||||
/// Opens Explore on an instance with the sample library.
|
||||
Future<void> exploreLibrary(WidgetTester tester) async {
|
||||
library.answer = sampleCatalogue();
|
||||
await start(tester, usedBefore: true);
|
||||
await goTab(tester, 'Explore');
|
||||
}
|
||||
|
||||
/// Opens the library item [title] from Explore.
|
||||
Future<void> openItem(WidgetTester tester, String title) async {
|
||||
await exploreLibrary(tester);
|
||||
await scrollTo(tester, find.text(title));
|
||||
await tester.tap(find.text(title));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets('an instance without a library shows nothing of one: no '
|
||||
'section in Explore, no credits in Settings', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await goTab(tester, 'Explore');
|
||||
await scrollTo(tester, find.text('Kind wishes'));
|
||||
expect(find.text('From meditate.example.org'), findsNothing);
|
||||
|
||||
await openSettings(tester);
|
||||
expect(find.text('Library credits'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("Explore shows the instance's courses and items below the "
|
||||
'practices, by category and by the search', (tester) async {
|
||||
await exploreLibrary(tester);
|
||||
|
||||
await scrollTo(tester, find.text('From meditate.example.org'));
|
||||
expect(find.text('A calm week'), findsOneWidget);
|
||||
expect(find.text('Course · 2 lessons'), findsOneWidget);
|
||||
await scrollTo(tester, find.text('Box breathing'));
|
||||
expect(find.text('Soft rain'), findsOneWidget);
|
||||
// The operator's own category, named as the manifest names it.
|
||||
expect(find.text('Breathwork'), findsWidgets);
|
||||
|
||||
await scrollTo(tester, find.text('Sounds'), up: true);
|
||||
await tester.tap(find.text('Sounds'));
|
||||
await tester.pumpAndSettle();
|
||||
await scrollTo(tester, find.text('Soft rain'));
|
||||
expect(find.text('Evening rest'), findsNothing);
|
||||
expect(find.text('A calm week'), findsNothing);
|
||||
|
||||
await scrollTo(tester, find.byType(TextField), up: true);
|
||||
await tester.enterText(find.byType(TextField), 'evening');
|
||||
await tester.pumpAndSettle();
|
||||
await scrollTo(tester, find.text('All').last);
|
||||
await tester.tap(find.text('All').last);
|
||||
await tester.pumpAndSettle();
|
||||
await scrollTo(tester, find.text('Evening rest'));
|
||||
expect(find.text('Soft rain'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('an item plays its recording chosen; played to its end it '
|
||||
'counts as a sitting', (tester) async {
|
||||
await openItem(tester, 'Evening rest');
|
||||
|
||||
expect(find.text('With Sample Speaker'), findsOneWidget);
|
||||
expect(
|
||||
find.text('Credit: Sample Speaker, for the example instance'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Licence: CC BY 4.0'), findsOneWidget);
|
||||
await tester.tap(find.text('Long'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Play · 20 min'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
const long =
|
||||
'https://meditate.example.org/library/'
|
||||
'bbbbbbbbbbbbbbbbbbbbbbbb.mp3?access=signed-access';
|
||||
expect(guide.calls, ['play $long']);
|
||||
expect(find.text('Pause'), findsOneWidget);
|
||||
await tester.tap(find.text('Pause'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(guide.calls.last, 'pause');
|
||||
await tester.tap(find.text('Resume'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
guide.finish();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Well sat'), findsOneWidget);
|
||||
final kept = await sittings().all();
|
||||
expect(kept.single.sitting.practiceId, 'library/evening-rest');
|
||||
expect(kept.single.sitting.satSeconds, 1200);
|
||||
});
|
||||
|
||||
testWidgets('stopped before its end, nothing is kept', (tester) async {
|
||||
await openItem(tester, 'Evening rest');
|
||||
await tester.tap(find.text('Play · 5 min'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byTooltip('Stop'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(guide.calls.last, 'stop');
|
||||
expect(await sittings().all(), isEmpty);
|
||||
});
|
||||
|
||||
testWidgets("a course's lessons, each ticked once played to its end, "
|
||||
'its done ones counted', (tester) async {
|
||||
await exploreLibrary(tester);
|
||||
await scrollTo(tester, find.text('A calm week'));
|
||||
await tester.tap(find.text('A calm week'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Meditation · 0 of 2 done'), findsOneWidget);
|
||||
expect(find.text('Evening one'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Evening one'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Play · 5 min'));
|
||||
await tester.pumpAndSettle();
|
||||
guide.finish();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Done'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Meditation · 1 of 2 done'), findsOneWidget);
|
||||
expect(find.byIcon(LucideIcons.check), findsOneWidget);
|
||||
expect(
|
||||
await LessonRepository(lessonRows, keys).done(),
|
||||
{(course: 'calm-week', item: 'evening-rest')},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('a favourite of the library shows on Today', (tester) async {
|
||||
await openItem(tester, 'Evening rest');
|
||||
await tester.tap(find.byTooltip('Add to favourites'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
(await settings().read()).favourites,
|
||||
['library/evening-rest'],
|
||||
);
|
||||
await goTab(tester, 'Today');
|
||||
await scrollTo(tester, find.text('Evening rest'));
|
||||
expect(find.text('Evening rest'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('kept on the device on asking, then played from there, and '
|
||||
'removed again', (tester) async {
|
||||
await openItem(tester, 'Soft rain');
|
||||
|
||||
await tester.tap(find.text('Keep on this device'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(downloads.fetched.keys, ['cccccccccccccccccccccccc.ogg']);
|
||||
expect(find.text('Remove from this device'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Play · 30 min'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
guide.calls.single,
|
||||
'play /device/library/cccccccccccccccccccccccc.ogg',
|
||||
);
|
||||
await tester.tap(find.byTooltip('Stop'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Remove from this device'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(downloads.fetched, isEmpty);
|
||||
expect(find.text('Keep on this device'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("one that can't be fetched says so", (tester) async {
|
||||
await openItem(tester, 'Soft rain');
|
||||
downloads.fails = true;
|
||||
|
||||
await tester.tap(find.text('Keep on this device'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.text("It couldn't be fetched. Try again when you're online."),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Keep on this device'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a sound plays looped under a silent sitting', (tester) async {
|
||||
await openItem(tester, 'Soft rain');
|
||||
expect(find.text('Sit in silence with this sound'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Sit in silence with this sound'));
|
||||
await sit(tester, const Duration(seconds: 2));
|
||||
|
||||
expect(find.byType(SittingScreen), findsOneWidget);
|
||||
const rain =
|
||||
'https://meditate.example.org/library/'
|
||||
'cccccccccccccccccccccccc.ogg?access=signed-access';
|
||||
expect(guide.calls, ['play $rain loop']);
|
||||
});
|
||||
|
||||
testWidgets("Settings lists the library's credits and licences", (
|
||||
tester,
|
||||
) async {
|
||||
library.answer = sampleCatalogue();
|
||||
await start(tester, usedBefore: true);
|
||||
await openSettings(tester);
|
||||
|
||||
await tapInSettings(tester, 'Library credits');
|
||||
|
||||
expect(find.textContaining('Credit: Sample Recordist'), findsOneWidget);
|
||||
expect(find.textContaining('Licence: CC0 1.0'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue