forked from Foxyz/sync_replay_project
Restructure step 4: local library and media identity
- L1 (core, offline): release names with kinds (episode, movie, extra: OP/ED/Opening/Ending/NC/PV/OVA…), folder hints (season and name from parentheticals, ranges), one show across folders, gaps and next with skipped numbers, junk ignored, stable keys. - L2 (app, device-owned cache): AniList match with synonyms and sequel walking, episode mapping across cours (AnimeFlow runs when signed in), art and episode titles, "Wrong show?" pins per show or folder, episode offsets; lazy, spaced requests, works offline. - Files tab as a library: poster cards, show pages with banner, gap prompt, extras, movies, Wrong show? and Episode numbers. - media.ref produced for local files, Jellyfin (provider ids) and links, carried in swaps and Up next; receivers name media from it. - Hosting relays each Jellyfin item through its own server's login. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4d00b7455e
commit
bf9fc193b3
44 files changed
+4350
-891
No files matched your search
@@ -1,5 +1,3 @@
|
||||
// restructure: rewrite in step 4 (Library + Media identity): series
|
||||
// grouping on the page follows L1/L2.
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
@@ -12,21 +10,54 @@ import 'package:starsync/src/files/scanner.dart';
|
||||
import 'package:starsync/src/files/thumbnails.dart';
|
||||
import 'package:starsync/src/files/ui/file_tiles.dart';
|
||||
import 'package:starsync/src/files/ui/files_page.dart';
|
||||
import 'package:starsync/src/files/ui/library_view.dart';
|
||||
import 'package:starsync/src/library/library_store.dart';
|
||||
import 'package:starsync/src/library/show_matcher.dart';
|
||||
import 'package:starsync/src/schedule/anilist.dart';
|
||||
|
||||
import 'test_graph.dart';
|
||||
|
||||
/// This file's composition root (see test_graph.dart).
|
||||
final _graph = testGraph();
|
||||
|
||||
// The Files page over a real (tiny) folder tree: the scan runs in-line
|
||||
// instead of in an isolate and thumbnails come from a fake loader, so
|
||||
// nothing here decodes video.
|
||||
// The Files tab as a library over a real (tiny) generated tree with the
|
||||
// names of a real anime folder: the scan runs in-line instead of in an
|
||||
// isolate, thumbnails come from a fake loader and AniList is a fake, so
|
||||
// nothing here decodes video or goes online.
|
||||
|
||||
final _tmp = Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path;
|
||||
|
||||
late Directory tmp;
|
||||
late String anime;
|
||||
late FolderStore store;
|
||||
late List<String> thumbRequests;
|
||||
late int scans;
|
||||
late ShowMatcher matcher;
|
||||
late FakeAniList aniList;
|
||||
|
||||
const b1 = '[Judas] Bleach 001-055 [BD 1080p][HEVC x265 10bit]';
|
||||
const b2 = '[Judas] Bleach 056-111 [BD 1080p][HEVC x265 10bit]';
|
||||
const b3 = '[Judas] Bleach 112-139 [BD 1080p][HEVC x265 10bit][Dual-Audio][Eng-Subs]';
|
||||
const sao = '[Judas] Sword Art Online (Season 3 - Alicization) [BD 1080p][HEVC x265 10bit][Eng-Subs]';
|
||||
const kimetsu =
|
||||
'[Erai-raws] Kimetsu no Yaiba - Mugenjou Hen - Movie 01 [1080p CR WEBRip HEVC AAC][MultiSub][8E47462C].mkv';
|
||||
|
||||
String bleach(int n) => '$anime/${n < 112 ? b2 : b3}/[Judas] Bleach - ${n.toString().padLeft(3, '0')}.mkv';
|
||||
|
||||
const _tybw = AniListEntry(
|
||||
id: 41467, romaji: 'BLEACH: Sennen Kessen-hen', english: 'Bleach: Thousand-Year Blood War', format: 'TV', episodes: 13);
|
||||
|
||||
class FakeAniList implements AniListLookup {
|
||||
final searches = <String>[];
|
||||
@override
|
||||
Future<List<AniListEntry>> searchAnime(String query) async {
|
||||
searches.add(query);
|
||||
return query.toLowerCase().contains('thousand') ? const [_tybw] : const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AniListEntry?> anime(int id) async => null;
|
||||
}
|
||||
|
||||
void touch(String path) => File(path)
|
||||
..createSync(recursive: true)
|
||||
@@ -56,6 +87,7 @@ Future<void> pumpPage(
|
||||
Future<String?> Function(String path)? onPlay,
|
||||
ThumbnailLoader? thumbnails,
|
||||
Future<String?> Function()? pickFolder,
|
||||
String? nowPlayingPath,
|
||||
}) async {
|
||||
tester.view.physicalSize = size;
|
||||
tester.view.devicePixelRatio = 1;
|
||||
@@ -69,45 +101,62 @@ Future<void> pumpPage(
|
||||
thumbnails: thumbnails ?? fakeThumb,
|
||||
scan: inlineScan,
|
||||
store: store,
|
||||
matcher: matcher,
|
||||
pickFolder: pickFolder,
|
||||
nowPlayingPath: nowPlayingPath,
|
||||
),
|
||||
),
|
||||
)));
|
||||
await frames(tester);
|
||||
}
|
||||
|
||||
Future<void> openFolder(WidgetTester tester, String name) async {
|
||||
Future<void> openPlace(WidgetTester tester, String name) async {
|
||||
await tester.tap(find.text(name).first);
|
||||
await frames(tester);
|
||||
}
|
||||
|
||||
/// Series start collapsed: a click on the header lists the episodes.
|
||||
Future<void> openSeries(WidgetTester tester, [String title = 'Show']) async {
|
||||
await tester.tap(find.descendant(of: find.byType(SeriesHeader), matching: find.text(title)));
|
||||
/// Scrolls the main list until [f] is on screen.
|
||||
Future<void> reveal(WidgetTester tester, Finder f) async {
|
||||
await tester.scrollUntilVisible(f, 150,
|
||||
scrollable: find.descendant(of: find.byType(CustomScrollView), matching: find.byType(Scrollable)).first);
|
||||
await frames(tester, 3);
|
||||
}
|
||||
|
||||
/// A show's card in the library opens its page.
|
||||
Future<void> openShow(WidgetTester tester, String title) async {
|
||||
final card = find.descendant(of: find.byType(PosterCard), matching: find.text(title));
|
||||
await reveal(tester, card);
|
||||
await tester.tap(card);
|
||||
await frames(tester);
|
||||
}
|
||||
|
||||
void main() {
|
||||
setUp(() async {
|
||||
tmp = Directory.systemTemp.createTempSync('files_page_');
|
||||
anime = '${tmp.path}/anime';
|
||||
for (final f in [
|
||||
'[Grp] Show - 01.mkv',
|
||||
'[Grp] Show - 02.mkv',
|
||||
'[Grp] Show - 03.mkv',
|
||||
'[Grp] Show - NCOP1.mkv',
|
||||
'[Grp] Show - 01.srt',
|
||||
'Some Movie (2019).mkv',
|
||||
'notes.txt',
|
||||
'extras/cover.jpg',
|
||||
]) {
|
||||
touch('$anime/$f');
|
||||
tmp = Directory(_tmp).createTempSync('files_page_');
|
||||
anime = '${tmp.path}/Binux';
|
||||
for (final j in ['App', 'Audio', 'Video']) {
|
||||
touch('$anime/$b1/$j#D4aIyEEtZEWR9gBorzBWiMh_.lnk');
|
||||
}
|
||||
for (final n in [56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 108, 109, 110, 111, 112, 113]) {
|
||||
touch(bleach(n));
|
||||
}
|
||||
for (var n = 1; n <= 3; n++) {
|
||||
touch('$anime/$sao/[Judas] Sword Art Online S3 - 0$n.mkv');
|
||||
}
|
||||
for (final x in ['Opening 1', 'Ending 1']) {
|
||||
touch('$anime/$sao/[Judas] Sword Art Online S3 - $x.mkv');
|
||||
}
|
||||
touch('$anime/$sao/[Judas] Sword Art Online S3 - 01.srt');
|
||||
touch('$anime/[Judas] Re.Zero - S04E17.mkv');
|
||||
touch('$anime/$kimetsu');
|
||||
touch('$anime/notes.txt');
|
||||
// An in-memory box: the store loads and writes through it, with no
|
||||
// file I/O left pending in the widget tests' fake-async zone.
|
||||
await Hive.openBox(filesBoxName, bytes: Uint8List(0));
|
||||
store = FolderStore();
|
||||
await store.ensureLoaded();
|
||||
aniList = FakeAniList();
|
||||
matcher = ShowMatcher(store: MemoryLibraryStore(), aniList: aniList, delay: (_) async {});
|
||||
thumbRequests = [];
|
||||
scans = 0;
|
||||
resetFilesPageSession();
|
||||
@@ -118,102 +167,145 @@ void main() {
|
||||
tmp.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
testWidgets('sidebar names folders by their last segment, header follows', (tester) async {
|
||||
store.addFolder('$anime/');
|
||||
await pumpPage(tester, pickFolder: () async => '${tmp.path}/movies');
|
||||
expect(find.text('Folders'), findsOneWidget);
|
||||
expect(find.text('anime'), findsOneWidget);
|
||||
expect(find.text('Recent'), findsNWidgets(2)); // sidebar + header
|
||||
testWidgets('the Library first: empty until a folder is added, then its shows', (tester) async {
|
||||
await pumpPage(tester, pickFolder: () async => anime);
|
||||
expect(find.text('Library'), findsNWidgets(2)); // sidebar + header
|
||||
expect(find.text('Your library is empty'), findsOneWidget);
|
||||
await tester.tap(find.text('Add folder').first);
|
||||
await frames(tester);
|
||||
expect(store.folders.value, [anime]);
|
||||
expect(find.text('Binux'), findsNWidgets(2)); // sidebar + header
|
||||
expect(scans, 1);
|
||||
// Back to the Library: the same scan, now grouped into shows.
|
||||
await openPlace(tester, 'Library');
|
||||
expect(scans, 1);
|
||||
expect(find.text('3 shows'), findsOneWidget); // sidebar
|
||||
expect(find.byType(PosterCard), findsNWidgets(4));
|
||||
expect(find.text('Shows'), findsOneWidget);
|
||||
expect(find.text('Movies'), findsOneWidget);
|
||||
|
||||
await openPlace(tester, 'Recent');
|
||||
expect(find.text('Nothing played yet'), findsOneWidget);
|
||||
expect(scans, 0, reason: 'folders are scanned when opened');
|
||||
|
||||
await openFolder(tester, 'anime');
|
||||
expect(find.text('anime'), findsNWidgets(2)); // sidebar + header
|
||||
expect(find.text('$anime/'), findsOneWidget); // full path under it
|
||||
expect(find.text('Recent'), findsOneWidget);
|
||||
expect(find.text('5 videos'), findsOneWidget); // sidebar subtitle
|
||||
expect(scans, 1);
|
||||
|
||||
// Back and forth: the scan is kept.
|
||||
await openFolder(tester, 'Recent');
|
||||
await openFolder(tester, 'anime');
|
||||
expect(scans, 1);
|
||||
|
||||
// Add folder: picked, stored, opened.
|
||||
Directory('${tmp.path}/movies').createSync();
|
||||
await tester.tap(find.text('Add folder'));
|
||||
await tester.tap(find.byTooltip('Remove folder'));
|
||||
await frames(tester);
|
||||
expect(store.folders.value, ['$anime/', '${tmp.path}/movies']);
|
||||
expect(find.text('movies'), findsNWidgets(2));
|
||||
expect(find.text('No videos here'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip('Remove folder').last);
|
||||
await frames(tester);
|
||||
expect(store.folders.value, ['$anime/']);
|
||||
expect(find.text('Recent'), findsNWidgets(2));
|
||||
expect(store.folders.value, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('episodes group into series, the rest is a card grid', (tester) async {
|
||||
testWidgets('shows, a movie, counts and what is hidden', (tester) async {
|
||||
store.addFolder(anime);
|
||||
await pumpPage(tester);
|
||||
await openFolder(tester, 'anime');
|
||||
expect(find.byType(SeriesHeader), findsOneWidget);
|
||||
expect(find.text('Show'), findsOneWidget);
|
||||
expect(find.text('3 episodes · 1 extra · Grp'), findsOneWidget);
|
||||
// Collapsed until clicked; open for the rest of the session.
|
||||
expect(find.text('E01'), findsNothing);
|
||||
await openSeries(tester);
|
||||
for (final l in ['E01', 'E02', 'E03', 'NCOP1']) {
|
||||
expect(find.text(l), findsOneWidget);
|
||||
for (final t in ['Bleach', 'Re.Zero', 'Sword Art Online']) {
|
||||
expect(find.descendant(of: find.byType(PosterCard), matching: find.text(t)), findsOneWidget);
|
||||
}
|
||||
expect(find.text('[Grp] Show - 02.mkv'), findsOneWidget);
|
||||
expect(find.byType(VideoCard), findsOneWidget);
|
||||
expect(find.text('Some Movie (2019)'), findsOneWidget);
|
||||
expect(find.text('Other videos'), findsOneWidget);
|
||||
// Durations come from the thumbnail.
|
||||
expect(find.text('24:00 · 1 KB'), findsNWidgets(4));
|
||||
expect(find.text('24:00'), findsOneWidget); // card badge
|
||||
expect(find.text('16 episodes'), findsOneWidget); // Bleach
|
||||
expect(find.text('Kimetsu no Yaiba - Mugenjou Hen'), findsOneWidget);
|
||||
expect(find.text('Movie 1 · 1 KB'), findsOneWidget);
|
||||
expect(find.text('23 videos in 3 shows · 4 other files hidden · 1 subtitle file'), findsOneWidget);
|
||||
expect(find.byType(VideoCard), findsNothing, reason: 'nothing is left over');
|
||||
|
||||
await openFolder(tester, 'Recent');
|
||||
await openFolder(tester, 'anime');
|
||||
expect(find.text('E01'), findsOneWidget);
|
||||
await openSeries(tester);
|
||||
expect(find.text('E01'), findsNothing);
|
||||
expect(find.byType(VideoCard), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('hidden files are counted and some can be shown', (tester) async {
|
||||
store.addFolder(anime);
|
||||
await pumpPage(tester);
|
||||
await openFolder(tester, 'anime');
|
||||
expect(find.text('5 videos in 1 series · 2 other files hidden · 1 subtitle file'),
|
||||
findsOneWidget);
|
||||
expect(find.text('notes.txt'), findsNothing);
|
||||
// The folder view says which files were left out.
|
||||
await openPlace(tester, 'Binux');
|
||||
await tester.tap(find.text('Show some'));
|
||||
await frames(tester);
|
||||
expect(find.text('notes.txt'), findsOneWidget);
|
||||
expect(find.text('cover.jpg'), findsOneWidget);
|
||||
expect(find.text('not a video'), findsNWidgets(2));
|
||||
await tester.tap(find.text('Hide them'));
|
||||
await frames(tester);
|
||||
expect(find.text('notes.txt'), findsNothing);
|
||||
expect(find.text('App#D4aIyEEtZEWR9gBorzBWiMh_.lnk'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('rescan picks up new files', (tester) async {
|
||||
testWidgets('a show page: season with its name, episodes, extras', (tester) async {
|
||||
store.addFolder(anime);
|
||||
await pumpPage(tester);
|
||||
await openFolder(tester, 'anime');
|
||||
await openSeries(tester);
|
||||
touch('$anime/[Grp] Show - 04.mkv');
|
||||
expect(find.text('E04'), findsNothing);
|
||||
await tester.tap(find.text('Rescan'));
|
||||
await openShow(tester, 'Sword Art Online');
|
||||
expect(find.text('Season 3 · Alicization'), findsOneWidget);
|
||||
for (final e in ['Episode 1', 'Episode 2', 'Episode 3']) {
|
||||
expect(find.text(e), findsOneWidget);
|
||||
}
|
||||
expect(find.text('Extras'), findsOneWidget);
|
||||
expect(find.text('OP1'), findsOneWidget);
|
||||
expect(find.text('ED1'), findsOneWidget);
|
||||
expect(find.text('Play: S3 E1'), findsOneWidget);
|
||||
expect(find.text('Not matched on AniList'), findsOneWidget);
|
||||
// The open show stays open for the session; Back returns.
|
||||
await openPlace(tester, 'Recent');
|
||||
await openPlace(tester, 'Library');
|
||||
expect(find.text('Season 3 · Alicization'), findsOneWidget);
|
||||
await tester.tap(find.text('Library').last);
|
||||
await frames(tester);
|
||||
expect(scans, 2);
|
||||
expect(find.text('E04'), findsOneWidget);
|
||||
expect(find.text('4 episodes · 1 extra · Grp'), findsOneWidget);
|
||||
expect(find.byType(PosterCard), findsNWidgets(4));
|
||||
});
|
||||
|
||||
testWidgets('a play goes to onPlay and into Recent', (tester) async {
|
||||
testWidgets('next episode knows what is missing: start at 56, then skip to 108', (tester) async {
|
||||
store.addFolder(anime);
|
||||
final played = <String>[];
|
||||
await pumpPage(tester, onPlay: (p) async {
|
||||
played.add(p);
|
||||
return null;
|
||||
});
|
||||
await openShow(tester, 'Bleach');
|
||||
expect(find.text('1–55 aren\'t in your folders — start at 56?'), findsOneWidget);
|
||||
await tester.tap(find.text('Start at 56'));
|
||||
await frames(tester);
|
||||
expect(played, [bleach(56)]);
|
||||
await reveal(tester, find.text('66–107 aren\'t in your folders')); // in the list
|
||||
|
||||
// Watched up to 65: the next one is 108, and the page says why.
|
||||
await reveal(tester, find.text('Episode 65'));
|
||||
await tester.tap(find.text('Episode 65'));
|
||||
await frames(tester);
|
||||
expect(store.recent.value.first, bleach(65));
|
||||
await tester.drag(find.byType(CustomScrollView), const Offset(0, 3000));
|
||||
await frames(tester, 3);
|
||||
expect(find.text('66–107 aren\'t in your folders — skip to 108?'), findsOneWidget);
|
||||
await tester.tap(find.text('Skip to 108'));
|
||||
await frames(tester);
|
||||
expect(played.last, bleach(108));
|
||||
expect(find.text('Next: 109'), findsOneWidget);
|
||||
|
||||
// The card says it too.
|
||||
await tester.tap(find.text('Library').last);
|
||||
await frames(tester);
|
||||
expect(find.text('Next: 109'), findsOneWidget);
|
||||
|
||||
// Recent names the show and episode.
|
||||
await openPlace(tester, 'Recent');
|
||||
expect(find.text('Bleach · E108'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Wrong show? pins the right AniList entry and the names follow', (tester) async {
|
||||
store.addFolder(anime);
|
||||
await pumpPage(tester);
|
||||
await openShow(tester, 'Bleach');
|
||||
await tester.tap(find.text('Wrong show?'));
|
||||
await frames(tester);
|
||||
await tester.enterText(find.byType(TextField), 'Bleach Thousand-Year Blood War');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await frames(tester);
|
||||
expect(aniList.searches.last, 'Bleach Thousand-Year Blood War');
|
||||
await tester.tap(find.text('Bleach: Thousand-Year Blood War'));
|
||||
await frames(tester);
|
||||
expect(matcher.showPin('bleach'), 41467);
|
||||
expect(find.text('Bleach: Thousand-Year Blood War'), findsOneWidget); // the page's title
|
||||
expect(find.text('pinned'), findsOneWidget);
|
||||
expect(find.textContaining('files say "Bleach"'), findsOneWidget);
|
||||
await tester.tap(find.text('Library').last);
|
||||
await frames(tester);
|
||||
expect(find.descendant(of: find.byType(PosterCard), matching: find.text('Bleach: Thousand-Year Blood War')),
|
||||
findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('episode numbers: the first file is episode N', (tester) async {
|
||||
store.addFolder(anime);
|
||||
await pumpPage(tester);
|
||||
await openShow(tester, 'Sword Art Online');
|
||||
await tester.tap(find.text('Episode numbers…'));
|
||||
await frames(tester);
|
||||
await tester.enterText(find.byKey(const ValueKey('offset:3')), '25');
|
||||
await tester.tap(find.text('Save'));
|
||||
await frames(tester);
|
||||
expect(matcher.offset('sword art online', 3), 24);
|
||||
expect(find.text('Episode 25'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a play goes to onPlay and into Recent; failures are shown, not kept', (tester) async {
|
||||
store.addFolder(anime);
|
||||
final played = <String>[];
|
||||
String? reply;
|
||||
@@ -222,66 +314,39 @@ void main() {
|
||||
return reply;
|
||||
});
|
||||
expect(find.textContaining('Keep the app open while it plays'), findsOneWidget);
|
||||
await openFolder(tester, 'anime');
|
||||
await openSeries(tester);
|
||||
|
||||
await tester.tap(find.text('E02'));
|
||||
await frames(tester);
|
||||
expect(played, ['$anime/[Grp] Show - 02.mkv']);
|
||||
expect(store.recent.value, ['$anime/[Grp] Show - 02.mkv']);
|
||||
|
||||
reply = 'Nobody can play mkv here';
|
||||
await tester.tap(find.text('Some Movie (2019)'));
|
||||
await reveal(tester, find.text('Kimetsu no Yaiba - Mugenjou Hen'));
|
||||
await tester.tap(find.text('Kimetsu no Yaiba - Mugenjou Hen'));
|
||||
await frames(tester);
|
||||
expect(played, hasLength(2));
|
||||
await tester.drag(find.byType(CustomScrollView), const Offset(0, 3000));
|
||||
await frames(tester);
|
||||
expect(played, ['$anime/$kimetsu']);
|
||||
expect(find.text('Nobody can play mkv here'), findsOneWidget);
|
||||
expect(store.recent.value, hasLength(1), reason: 'failed plays are not recent');
|
||||
|
||||
await openFolder(tester, 'Recent');
|
||||
expect(find.text('Show · E02'), findsOneWidget);
|
||||
expect(find.text('[Grp] Show - 02.mkv'), findsOneWidget);
|
||||
expect(store.recent.value, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('now playing is marked', (tester) async {
|
||||
testWidgets('now playing is marked on its show and row', (tester) async {
|
||||
store.addFolder(anime);
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await tester.pumpWidget(scoped(_graph, MaterialApp(
|
||||
home: Scaffold(
|
||||
body: FilesPage(
|
||||
relayed: false,
|
||||
onPlay: (_) async => null,
|
||||
nowPlayingPath: '$anime/[Grp] Show - 03.mkv',
|
||||
thumbnails: fakeThumb,
|
||||
scan: inlineScan,
|
||||
store: store,
|
||||
),
|
||||
),
|
||||
)));
|
||||
await frames(tester);
|
||||
await openFolder(tester, 'anime');
|
||||
await openSeries(tester);
|
||||
await pumpPage(tester, nowPlayingPath: '$anime/$sao/[Judas] Sword Art Online S3 - 02.mkv');
|
||||
await openShow(tester, 'Sword Art Online');
|
||||
expect(find.text('now playing'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.graphic_eq), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('narrow: folders become chips above the list', (tester) async {
|
||||
testWidgets('narrow: places become chips above the list', (tester) async {
|
||||
store.addFolder('$anime/');
|
||||
await pumpPage(tester, size: const Size(600, 800));
|
||||
expect(find.text('Folders'), findsNothing);
|
||||
expect(find.widgetWithText(InputChip, 'anime'), findsOneWidget);
|
||||
expect(find.widgetWithText(ChoiceChip, 'Library'), findsOneWidget);
|
||||
expect(find.widgetWithText(ChoiceChip, 'Recent'), findsOneWidget);
|
||||
await tester.tap(find.widgetWithText(InputChip, 'anime'));
|
||||
await frames(tester);
|
||||
expect(find.text('$anime/'), findsOneWidget);
|
||||
await openSeries(tester);
|
||||
expect(find.text('E01'), findsOneWidget);
|
||||
// The chip's x removes the folder, like the sidebar's.
|
||||
expect(find.widgetWithText(InputChip, 'Binux'), findsOneWidget);
|
||||
await openShow(tester, 'Re.Zero');
|
||||
expect(find.text('Season 4'), findsOneWidget);
|
||||
expect(find.text('16 isn\'t in your folders'), findsNothing);
|
||||
expect(find.text('1–16 aren\'t in your folders — start at 17?'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('Remove folder'));
|
||||
await frames(tester);
|
||||
expect(store.folders.value, isEmpty);
|
||||
expect(find.widgetWithText(InputChip, 'anime'), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
@@ -289,53 +354,52 @@ void main() {
|
||||
testWidgets('no overflow at ${size.width.toInt()}x${size.height.toInt()}', (tester) async {
|
||||
store.addFolder(anime);
|
||||
for (var i = 0; i < 7; i++) {
|
||||
touch('$anime/A Movie With A Rather Long Title Indeed Part $i (2020).mkv');
|
||||
touch('$anime/clips/A Clip With A Rather Long Title Indeed Part $i.mkv');
|
||||
}
|
||||
store.addRecent('$anime/[Grp] Show - 01.mkv');
|
||||
store.addRecent(bleach(56));
|
||||
await pumpPage(tester, size: size, relayed: true);
|
||||
expect(tester.takeException(), isNull);
|
||||
await tester.tap(find.text('anime').first);
|
||||
await tester.drag(find.byType(CustomScrollView), const Offset(0, -2000));
|
||||
await frames(tester);
|
||||
await openSeries(tester);
|
||||
expect(find.text('E01'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
await tester.tap(find.text('Show some'));
|
||||
await tester.drag(find.byType(CustomScrollView), const Offset(0, 4000));
|
||||
await frames(tester);
|
||||
await openShow(tester, 'Bleach');
|
||||
await reveal(tester, find.text('Episode 56'));
|
||||
expect(tester.takeException(), isNull);
|
||||
await tester.drag(find.byType(CustomScrollView), const Offset(0, -2000));
|
||||
await frames(tester);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets('thumbnails load for visible tiles only, and cancel when scrolled away',
|
||||
(tester) async {
|
||||
testWidgets('thumbnails load for visible rows only, and cancel when scrolled away', (tester) async {
|
||||
final big = '${tmp.path}/big';
|
||||
for (var i = 1; i <= 120; i++) {
|
||||
touch('$big/Long Show - ${i.toString().padLeft(3, '0')}.mkv');
|
||||
}
|
||||
store.addFolder(big);
|
||||
final gates = <String, bool Function()>{};
|
||||
await pumpPage(tester, size: const Size(1100, 700),
|
||||
thumbnails: (path, {cancelled}) {
|
||||
await pumpPage(tester, size: const Size(1100, 700), thumbnails: (path, {cancelled}) {
|
||||
thumbRequests.add(path);
|
||||
gates[path] = cancelled!;
|
||||
return Completer<Thumbnail?>().future; // never finishes
|
||||
});
|
||||
await openFolder(tester, 'big');
|
||||
await openSeries(tester, 'Long Show');
|
||||
expect(find.text('E01'), findsOneWidget);
|
||||
await openShow(tester, 'Long Show');
|
||||
expect(find.text('Episode 1'), findsOneWidget);
|
||||
final first = thumbRequests.toSet();
|
||||
expect(first, isNotEmpty);
|
||||
expect(first.length, lessThan(30));
|
||||
expect(first, contains('$big/Long Show - 001.mkv'));
|
||||
expect(gates['$big/Long Show - 001.mkv']!(), isFalse);
|
||||
// (Episode 1's frame is also the header's banner, which stays.)
|
||||
expect(first, contains('$big/Long Show - 002.mkv'));
|
||||
expect(gates['$big/Long Show - 002.mkv']!(), isFalse);
|
||||
|
||||
// Fly past the middle: tiles that only flash by never ask.
|
||||
// Fly past the middle: rows that only flash by never ask.
|
||||
await tester.drag(find.byType(CustomScrollView), const Offset(0, -6000));
|
||||
await tester.pump();
|
||||
await tester.drag(find.byType(CustomScrollView), const Offset(0, -6000));
|
||||
await frames(tester);
|
||||
expect(gates['$big/Long Show - 001.mkv']!(), isTrue);
|
||||
expect(gates['$big/Long Show - 002.mkv']!(), isTrue);
|
||||
expect(thumbRequests, contains('$big/Long Show - 120.mkv'));
|
||||
expect(thumbRequests.toSet().length, lessThan(60));
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user