- What's next is the server's: state.advance counts down (10 s) when the media finishes with an Up next item first, holds while its performer or warm source is gone, and sets queuePlay at its end; cancel is shared, a seek or swap ends it; the server skips items nobody can put up. Shared rules in core whats_next.dart; the app's janitor, per-screen countdown and end-of-episode card are gone. - The continuation (next Jellyfin episode, next file of a local show with its gaps) is an auto item under "Continues with", prepared like Up next's first item. - Watch tracking by time In (D3): only time played while this viewer is in counts; away, paused, held, jumps and stalls never do (P7, P9). WatchTracker + device WatchHistory (files, links); Jellyfin progress reads the same watch; records are the AniList seam. - A server restart no longer loses what played: apps hold their media and one offers it back (swap.restore, first offer wins), Up next is re-added, the source re-feeds, and the watch carries on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
48 lines
1.7 KiB
Dart
48 lines
1.7 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
/// The app's own `.instance` singletons and how often `lib` reaches for
|
|
/// each. Systems get their dependencies through the composition root
|
|
/// (`AppGraph`, widgets through `AppScope`); this list may only shrink.
|
|
/// Lower a number when a use goes; never raise one or add a class.
|
|
const baseline = {
|
|
'AppSettings': 49,
|
|
'JellyfinAccounts': 10,
|
|
'UpdateController': 8,
|
|
'Profile': 8,
|
|
'FolderStore': 6,
|
|
'AppYtDlp': 2,
|
|
'RecentLinks': 3,
|
|
};
|
|
|
|
void main() {
|
|
test('no new .instance singletons, and no more uses of the old ones', () {
|
|
final files = Directory('lib')
|
|
.listSync(recursive: true)
|
|
.whereType<File>()
|
|
.where((f) => f.path.endsWith('.dart'))
|
|
.toList();
|
|
final sources = [for (final f in files) f.readAsStringSync()];
|
|
// The app's own classes (framework singletons like
|
|
// `WidgetsBinding.instance` are not counted).
|
|
final classes = {
|
|
for (final s in sources)
|
|
for (final m in RegExp(r'^(?:abstract |final |base )*class (\w+)', multiLine: true).allMatches(s)) m.group(1)!,
|
|
};
|
|
final counts = <String, int>{};
|
|
for (final s in sources) {
|
|
for (final m in RegExp(r'\b([A-Z]\w*)\.instance\b').allMatches(s)) {
|
|
final name = m.group(1)!;
|
|
if (classes.contains(name)) counts[name] = (counts[name] ?? 0) + 1;
|
|
}
|
|
}
|
|
final grown = {
|
|
for (final e in counts.entries)
|
|
if (e.value > (baseline[e.key] ?? 0)) e.key: '${e.value} (baseline ${baseline[e.key] ?? 'none'})',
|
|
};
|
|
expect(grown, isEmpty,
|
|
reason: 'pass the system in through AppGraph / AppScope instead of a new `.instance` use or singleton');
|
|
});
|
|
}
|