Schedule: remove the per-user follow (bookmark, Following filter, AnimeFlow follow calls)

Personal tracking will come from the user's AniList lists; AnimeFlow keeps
downloads, availability and requests only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-10 20:51:42 +03:00
1 parent 473c82ec0a
commit f23cc7b311
8 files changed
+22 -130

No files matched your search

@@ -248,7 +248,6 @@ class AnimeFlowScheduleItem {
final int? anidbId; final int? anidbId;
final int? tvdbId; final int? tvdbId;
final String? jellyfinSeriesId; final String? jellyfinSeriesId;
final bool followed;
const AnimeFlowScheduleItem({ const AnimeFlowScheduleItem({
required this.episode, required this.episode,
@@ -257,7 +256,6 @@ class AnimeFlowScheduleItem {
this.anidbId, this.anidbId,
this.tvdbId, this.tvdbId,
this.jellyfinSeriesId, this.jellyfinSeriesId,
this.followed = false,
}); });
static AnimeFlowScheduleItem fromJson(Map j) => AnimeFlowScheduleItem( static AnimeFlowScheduleItem fromJson(Map j) => AnimeFlowScheduleItem(
@@ -267,7 +265,6 @@ class AnimeFlowScheduleItem {
anidbId: _int(j['anidb_id']), anidbId: _int(j['anidb_id']),
tvdbId: _int(j['tvdb_id']), tvdbId: _int(j['tvdb_id']),
jellyfinSeriesId: _str(j['jellyfin_series_id']), jellyfinSeriesId: _str(j['jellyfin_series_id']),
followed: j['followed'] == true,
); );
} }
@@ -296,7 +293,6 @@ class AnimeFlowSeries {
final String? jellyfinSeriesId; final String? jellyfinSeriesId;
final SeriesState state; final SeriesState state;
final bool monitored; final bool monitored;
final bool followed;
final int episodesTotal; final int episodesTotal;
final int episodesAvailable; final int episodesAvailable;
final DateTime? nextAirDate; final DateTime? nextAirDate;
@@ -312,7 +308,6 @@ class AnimeFlowSeries {
this.jellyfinSeriesId, this.jellyfinSeriesId,
this.state = SeriesState.releasing, this.state = SeriesState.releasing,
this.monitored = false, this.monitored = false,
this.followed = false,
this.episodesTotal = 0, this.episodesTotal = 0,
this.episodesAvailable = 0, this.episodesAvailable = 0,
this.nextAirDate, this.nextAirDate,
@@ -328,7 +323,6 @@ class AnimeFlowSeries {
jellyfinSeriesId: _str(j['jellyfin_series_id']), jellyfinSeriesId: _str(j['jellyfin_series_id']),
state: SeriesState.parse(j['state']), state: SeriesState.parse(j['state']),
monitored: j['monitored'] == true, monitored: j['monitored'] == true,
followed: j['followed'] == true,
episodesTotal: _int(j['episodes_total']) ?? 0, episodesTotal: _int(j['episodes_total']) ?? 0,
episodesAvailable: _int(j['episodes_available']) ?? 0, episodesAvailable: _int(j['episodes_available']) ?? 0,
nextAirDate: _time(j['next_air_date']), nextAirDate: _time(j['next_air_date']),
@@ -684,10 +678,6 @@ class AnimeFlowClient {
return AnimeFlowSeries.fromJson(j is Map ? j : const {}); return AnimeFlowSeries.fromJson(j is Map ? j : const {});
} }
/// Follow (or unfollow) a series [view].
Future<void> setFollowed(int seriesId, bool followed) =>
_call(followed ? 'PUT' : 'DELETE', '/series/$seriesId/follow');
/// Titles that can be requested [request]. /// Titles that can be requested [request].
Future<List<AnimeFlowSearchHit>> search(String q) async => [ Future<List<AnimeFlowSearchHit>> search(String q) async => [
for (final h in _items(await _call('GET', '/search', query: {'q': q}))) AnimeFlowSearchHit.fromJson(h), for (final h in _items(await _call('GET', '/search', query: {'q': q}))) AnimeFlowSearchHit.fromJson(h),
@@ -48,8 +48,7 @@ class ScheduleEntry {
final String? reason; final String? reason;
/// AnimeFlow tracks the series. /// AnimeFlow tracks the series.
final bool tracked; final bool tracked;
final bool followed; /// AnimeFlow's series id (requests).
/// AnimeFlow's series id (follow, requests).
final int? seriesId; final int? seriesId;
/// The AnimeFlow season of the matched episode (requests); untracked /// The AnimeFlow season of the matched episode (requests); untracked
/// on Jellyfin: the TVDB season of the AniList entry. /// on Jellyfin: the TVDB season of the AniList entry.
@@ -68,7 +67,6 @@ class ScheduleEntry {
this.progress, this.progress,
this.reason, this.reason,
this.tracked = false, this.tracked = false,
this.followed = false,
this.seriesId, this.seriesId,
this.season, this.season,
this.jellyfinEpisode, this.jellyfinEpisode,
@@ -81,7 +79,6 @@ class ScheduleEntry {
progress: progress, progress: progress,
reason: reason, reason: reason,
tracked: tracked, tracked: tracked,
followed: followed,
seriesId: seriesId, seriesId: seriesId,
season: season, season: season,
jellyfinEpisode: jellyfinEpisode, jellyfinEpisode: jellyfinEpisode,
@@ -106,8 +103,7 @@ String _day(DateTime d) {
class _SeriesRef { class _SeriesRef {
final int seriesId; final int seriesId;
final String? jellyfinSeriesId; final String? jellyfinSeriesId;
bool followed; _SeriesRef(this.seriesId, this.jellyfinSeriesId);
_SeriesRef(this.seriesId, this.jellyfinSeriesId, this.followed);
} }
/// Merges AniList [airings] with AnimeFlow's [schedule] rows and tracked /// Merges AniList [airings] with AnimeFlow's [schedule] rows and tracked
@@ -138,7 +134,7 @@ List<ScheduleEntry> matchSchedule(
final refs = <int, _SeriesRef>{}; final refs = <int, _SeriesRef>{};
for (final s in series) { for (final s in series) {
final id = s.anilistId; final id = s.anilistId;
if (id != null) refs[id] ??= _SeriesRef(s.seriesId, s.jellyfinSeriesId, s.followed); if (id != null) refs[id] ??= _SeriesRef(s.seriesId, s.jellyfinSeriesId);
} }
// AnimeFlow's AniList episode number wins outright: TVDB numbering runs // AnimeFlow's AniList episode number wins outright: TVDB numbering runs
// on across split entries (JoJo S6E4 is "2nd & 3rd STAGE" ep 3), so a TVDB // on across split entries (JoJo S6E4 is "2nd & 3rd STAGE" ep 3), so a TVDB
@@ -152,8 +148,7 @@ List<ScheduleEntry> matchSchedule(
for (final it in schedule) { for (final it in schedule) {
final e = it.episode, id = e.anilistId; final e = it.episode, id = e.anilistId;
if (id == null) continue; if (id == null) continue;
final ref = refs[id] ??= _SeriesRef(it.seriesId, it.jellyfinSeriesId, it.followed); refs[id] ??= _SeriesRef(it.seriesId, it.jellyfinSeriesId);
if (it.followed) ref.followed = true;
if (e.anilistEpisode != null) { if (e.anilistEpisode != null) {
numbered.add(id); numbered.add(id);
byAni.putIfAbsent('$id#${e.anilistEpisode}', () => it); byAni.putIfAbsent('$id#${e.anilistEpisode}', () => it);
@@ -178,7 +173,6 @@ List<ScheduleEntry> matchSchedule(
progress: e.status == EpisodeStatus.downloading ? e.progress : null, progress: e.status == EpisodeStatus.downloading ? e.progress : null,
reason: e.status == EpisodeStatus.failed ? e.reason : null, reason: e.status == EpisodeStatus.failed ? e.reason : null,
tracked: true, tracked: true,
followed: it.followed || (ref?.followed ?? false),
seriesId: it.seriesId, seriesId: it.seriesId,
season: e.season, season: e.season,
jellyfinSeriesId: it.jellyfinSeriesId ?? ref?.jellyfinSeriesId ?? lib[id]?.$1, jellyfinSeriesId: it.jellyfinSeriesId ?? ref?.jellyfinSeriesId ?? lib[id]?.$1,
@@ -199,7 +193,6 @@ List<ScheduleEntry> matchSchedule(
airing: a, airing: a,
status: a.airingAt.isAfter(at) ? ScheduleStatus.upcoming : ScheduleStatus.missing, status: a.airingAt.isAfter(at) ? ScheduleStatus.upcoming : ScheduleStatus.missing,
tracked: true, tracked: true,
followed: ref.followed,
seriesId: ref.seriesId, seriesId: ref.seriesId,
jellyfinSeriesId: ref.jellyfinSeriesId ?? lib[id]?.$1, jellyfinSeriesId: ref.jellyfinSeriesId ?? lib[id]?.$1,
); );
@@ -127,7 +127,6 @@ class AiringCard extends StatelessWidget {
final ScheduleEntry entry; final ScheduleEntry entry;
final DateTime now; final DateTime now;
final bool compact; final bool compact;
final bool followed;
final CardAction action; final CardAction action;
/// Why [action] is disabled (no request scope, not on Jellyfin, ...); /// Why [action] is disabled (no request scope, not on Jellyfin, ...);
@@ -138,20 +137,15 @@ class AiringCard extends StatelessWidget {
final bool busy; final bool busy;
final VoidCallback onAction; final VoidCallback onAction;
/// Null hides the bookmark (untracked series).
final VoidCallback? onFollow;
const AiringCard({ const AiringCard({
super.key, super.key,
required this.entry, required this.entry,
required this.now, required this.now,
required this.compact, required this.compact,
required this.followed,
required this.action, required this.action,
required this.onAction, required this.onAction,
this.disabledHint, this.disabledHint,
this.busy = false, this.busy = false,
this.onFollow,
}); });
String get _id => '${entry.airing.anilistId}-${entry.airing.episode}'; String get _id => '${entry.airing.anilistId}-${entry.airing.episode}';
@@ -181,22 +175,6 @@ class AiringCard extends StatelessWidget {
return disabledHint == null ? b : Tooltip(message: disabledHint!, child: b); return disabledHint == null ? b : Tooltip(message: disabledHint!, child: b);
} }
Widget? _follow(BuildContext context, double size) {
final f = onFollow;
if (f == null) return null;
return IconButton(
key: ValueKey('follow-$_id'),
onPressed: f,
tooltip: followed ? 'Stop following' : 'Follow',
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
constraints: BoxConstraints.tightFor(width: size + 10, height: size + 10),
iconSize: size,
color: followed ? Theme.of(context).colorScheme.primary : null,
icon: Icon(followed ? Icons.bookmark : Icons.bookmark_outline),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme; final scheme = Theme.of(context).colorScheme;
@@ -246,7 +224,6 @@ class AiringCard extends StatelessWidget {
], ],
), ),
), ),
if (followed && onFollow == null) Icon(Icons.bookmark, size: 14, color: scheme.primary),
if (requestable) if (requestable)
Tooltip( Tooltip(
message: disabledHint ?? (action == CardAction.request ? 'Request' : 'Request series'), message: disabledHint ?? (action == CardAction.request ? 'Request' : 'Request series'),
@@ -270,7 +247,6 @@ class AiringCard extends StatelessWidget {
); );
} }
final button = _button(context); final button = _button(context);
final follow = _follow(context, 16);
// The cover poster leads: it is what people recognise at a glance. // The cover poster leads: it is what people recognise at a glance.
return Material( return Material(
key: ValueKey('card-$_id'), key: ValueKey('card-$_id'),
@@ -340,25 +316,11 @@ class AiringCard extends StatelessWidget {
style: const TextStyle(fontWeight: FontWeight.w600, height: 1.25), style: const TextStyle(fontWeight: FontWeight.w600, height: 1.25),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
Row( Text(
children: [ 'Episode ${a.episode}',
Flexible( maxLines: 1,
child: Text( overflow: TextOverflow.ellipsis,
'Episode ${a.episode}', style: TextStyle(color: scheme.onSurfaceVariant, fontSize: 12),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: scheme.onSurfaceVariant, fontSize: 12),
),
),
const SizedBox(width: 2),
if (follow != null)
follow
else if (followed)
Padding(
padding: const EdgeInsets.only(left: 4),
child: Icon(Icons.bookmark, size: 14, color: scheme.primary),
),
],
), ),
const Spacer(), const Spacer(),
Row( Row(
@@ -18,7 +18,7 @@ import 'schedule_card.dart';
// one thing to do about it (put it up, request it). Refreshes every // one thing to do about it (put it up, request it). Refreshes every
// minute while visible; ETags keep that cheap. // minute while visible; ETags keep that cheap.
enum ScheduleFilter { jellyfin, following, all } enum ScheduleFilter { jellyfin, all }
class SchedulePage extends StatefulWidget { class SchedulePage extends StatefulWidget {
/// Nobody else watches (offline or solo): available episodes say Play /// Nobody else watches (offline or solo): available episodes say Play
@@ -75,8 +75,6 @@ class _SchedulePageState extends State<SchedulePage> {
/// The sheet stays built while it slides out. /// The sheet stays built while it slides out.
bool _sheetBuilt = false; bool _sheetBuilt = false;
/// Follow toggles not yet confirmed by a reload (series id -> followed).
final _followOverride = <int, bool>{};
/// Requested in this run: "anilistId#episode" or "series:anilistId". /// Requested in this run: "anilistId#episode" or "series:anilistId".
final _requested = <String>{}; final _requested = <String>{};
@@ -190,11 +188,8 @@ class _SchedulePageState extends State<SchedulePage> {
animate ? _scroll.animateTo(x, duration: easeTime, curve: ease) : _scroll.jumpTo(x); animate ? _scroll.animateTo(x, duration: easeTime, curve: ease) : _scroll.jumpTo(x);
} }
bool _followed(ScheduleEntry e) => (e.seriesId == null ? null : _followOverride[e.seriesId]) ?? e.followed;
bool _shows(ScheduleEntry e) => switch (_filter) { bool _shows(ScheduleEntry e) => switch (_filter) {
ScheduleFilter.jellyfin => e.tracked || e.onJellyfin, ScheduleFilter.jellyfin => e.tracked || e.onJellyfin,
ScheduleFilter.following => _followed(e),
ScheduleFilter.all => _niche || e.tracked || e.airing.popularity >= SchedulePage.nicheBelow, ScheduleFilter.all => _niche || e.tracked || e.airing.popularity >= SchedulePage.nicheBelow,
}; };
@@ -341,21 +336,6 @@ class _SchedulePageState extends State<SchedulePage> {
}, },
); );
Future<void> _toggleFollow(ScheduleEntry e) async {
final c = _flow, id = e.seriesId;
if (c == null || id == null) return;
final next = !_followed(e);
setState(() => _followOverride[id] = next);
try {
await c.setFollowed(id, next);
await _load();
} catch (err) {
_toast(friendlyError(err));
} finally {
if (mounted) setState(() => _followOverride.remove(id));
}
}
Future<void> _decide(AnimeFlowRequest r, {required bool approve}) async { Future<void> _decide(AnimeFlowRequest r, {required bool approve}) async {
final c = _flow; final c = _flow;
if (c == null || _reqBusy.contains(r.id)) return; if (c == null || _reqBusy.contains(r.id)) return;
@@ -393,11 +373,6 @@ class _SchedulePageState extends State<SchedulePage> {
icon: Icon(Icons.video_library_outlined), icon: Icon(Icons.video_library_outlined),
label: Text('On Jellyfin'), label: Text('On Jellyfin'),
), ),
ButtonSegment(
value: ScheduleFilter.following,
icon: Icon(Icons.bookmark_outline),
label: Text('Following'),
),
ButtonSegment(value: ScheduleFilter.all, icon: Icon(Icons.public), label: Text('All airing')), ButtonSegment(value: ScheduleFilter.all, icon: Icon(Icons.public), label: Text('All airing')),
], ],
selected: {_filter}, selected: {_filter},
@@ -541,12 +516,10 @@ class _SchedulePageState extends State<SchedulePage> {
entry: e, entry: e,
now: now, now: now,
compact: _compact, compact: _compact,
followed: _followed(e),
action: action, action: action,
disabledHint: hint, disabledHint: hint,
busy: busy, busy: busy,
onAction: () => _onAction(e, action), onAction: () => _onAction(e, action),
onFollow: e.tracked && e.seriesId != null && _flow != null ? () => _toggleFollow(e) : null,
); );
} }
+3 -11
View File
@@ -29,7 +29,6 @@ final seriesJson = {
'jellyfin_series_id': 'jf7', 'jellyfin_series_id': 'jf7',
'state': 'released', 'state': 'released',
'monitored': true, 'monitored': true,
'followed': true,
'episodes_total': 24, 'episodes_total': 24,
'episodes_available': 20, 'episodes_available': 20,
'next_air_date': null, 'next_air_date': null,
@@ -110,7 +109,7 @@ void main() {
'to': req.uri.queryParameters['to'], 'to': req.uri.queryParameters['to'],
'items': [ 'items': [
{...episodeJson, 'series_id': 7, 'title': 'Show', 'anidb_id': 70, 'tvdb_id': 700, {...episodeJson, 'series_id': 7, 'title': 'Show', 'anidb_id': 70, 'tvdb_id': 700,
'jellyfin_series_id': 'jf7', 'followed': true}, 'jellyfin_series_id': 'jf7'},
], ],
}, etag: 'W/"s1"'); }, etag: 'W/"s1"');
}, },
@@ -126,7 +125,6 @@ void main() {
expect(it.anidbId, 70); expect(it.anidbId, 70);
expect(it.tvdbId, 700); expect(it.tvdbId, 700);
expect(it.jellyfinSeriesId, 'jf7'); expect(it.jellyfinSeriesId, 'jf7');
expect(it.followed, isTrue);
final e = it.episode; final e = it.episode;
expect((e.season, e.episode, e.absolute, e.title, e.anilistId), (2, 3, 15, 'Three', 222)); expect((e.season, e.episode, e.absolute, e.title, e.anilistId), (2, 3, 15, 'Three', 222));
expect(e.airDate, DateTime.utc(2026, 10, 8, 19, 36, 32)); expect(e.airDate, DateTime.utc(2026, 10, 8, 19, 36, 32));
@@ -146,7 +144,7 @@ void main() {
expect(server.seen, isEmpty); expect(server.seen, isEmpty);
}); });
test('/series, /series/{id} with episodes, follow/unfollow', () async { test('/series, /series/{id} with episodes', () async {
await serve({ await serve({
'GET /series': (req, res) async => json(res, {'items': [seriesJson]}, etag: 'W/"a"'), 'GET /series': (req, res) async => json(res, {'items': [seriesJson]}, etag: 'W/"a"'),
'GET /series/7': (req, res) async => json(res, { 'GET /series/7': (req, res) async => json(res, {
@@ -156,24 +154,18 @@ void main() {
{'season': 1, 'episode': 1, 'status': 'failed', 'reason': 'No release'}, {'season': 1, 'episode': 1, 'status': 'failed', 'reason': 'No release'},
], ],
}), }),
'PUT /series/7/follow': (req, res) async => res.statusCode = 204,
'DELETE /series/7/follow': (req, res) async => res.statusCode = 204,
}); });
final c = client(); final c = client();
final s = (await c.series()).single; final s = (await c.series()).single;
expect(s.seriesId, 7); expect(s.seriesId, 7);
expect(s.anilistId, 111); expect(s.anilistId, 111);
expect(s.state, SeriesState.released); expect(s.state, SeriesState.released);
expect(s.monitored && s.followed, isTrue); expect(s.monitored, isTrue);
expect((s.episodesTotal, s.episodesAvailable, s.nextAirDate), (24, 20, null)); expect((s.episodesTotal, s.episodesAvailable, s.nextAirDate), (24, 20, null));
final d = await c.seriesDetail(7); final d = await c.seriesDetail(7);
expect(d.episodes.map((e) => e.season), [0, 1]); expect(d.episodes.map((e) => e.season), [0, 1]);
expect(d.episodes.last.status, EpisodeStatus.failed); expect(d.episodes.last.status, EpisodeStatus.failed);
expect(d.episodes.last.reason, 'No release'); expect(d.episodes.last.reason, 'No release');
await c.setFollowed(7, true);
await c.setFollowed(7, false);
expect(server.seen.skip(2).map((r) => '${r.method} ${r.uri.path}'),
['PUT /api/app/v1/series/7/follow', 'DELETE /api/app/v1/series/7/follow']);
}); });
test('/search and the three request bodies', () async { test('/search and the three request bodies', () async {
+5 -7
View File
@@ -20,7 +20,7 @@ Airing airing(int id, int ep, DateTime local) => Airing(
); );
AnimeFlowScheduleItem item(int? anilistId, int ep, EpisodeStatus status, AnimeFlowScheduleItem item(int? anilistId, int ep, EpisodeStatus status,
{int season = 1, int? absolute, DateTime? air, int seriesId = 7, bool followed = false, {int season = 1, int? absolute, DateTime? air, int seriesId = 7,
double? progress, String? reason}) => double? progress, String? reason}) =>
AnimeFlowScheduleItem( AnimeFlowScheduleItem(
episode: AnimeFlowEpisode( episode: AnimeFlowEpisode(
@@ -36,7 +36,6 @@ AnimeFlowScheduleItem item(int? anilistId, int ep, EpisodeStatus status,
seriesId: seriesId, seriesId: seriesId,
title: 'Show', title: 'Show',
jellyfinSeriesId: 'jf$seriesId', jellyfinSeriesId: 'jf$seriesId',
followed: followed,
); );
void main() { void main() {
@@ -150,7 +149,7 @@ void main() {
[airing(100, 3, DateTime(2026, 10, 1, 20)), airing(200, 3, DateTime(2026, 10, 9, 20))], [airing(100, 3, DateTime(2026, 10, 1, 20)), airing(200, 3, DateTime(2026, 10, 9, 20))],
schedule: [ schedule: [
item(100, 3, EpisodeStatus.available, season: 1), item(100, 3, EpisodeStatus.available, season: 1),
item(200, 3, EpisodeStatus.downloading, season: 2, progress: 0.4, followed: true), item(200, 3, EpisodeStatus.downloading, season: 2, progress: 0.4),
], ],
series: [const AnimeFlowSeries(seriesId: 7, title: 'Show', anilistId: 100, jellyfinSeriesId: 'jf7')], series: [const AnimeFlowSeries(seriesId: 7, title: 'Show', anilistId: 100, jellyfinSeriesId: 'jf7')],
now: now, now: now,
@@ -158,7 +157,6 @@ void main() {
expect(out.map((e) => e.status), [ScheduleStatus.available, ScheduleStatus.downloading]); expect(out.map((e) => e.status), [ScheduleStatus.available, ScheduleStatus.downloading]);
expect(out.map((e) => e.season), [1, 2]); expect(out.map((e) => e.season), [1, 2]);
expect(out[1].progress, 0.4); expect(out[1].progress, 0.4);
expect(out[1].followed, isTrue);
expect(out.every((e) => e.tracked && e.seriesId == 7 && e.jellyfinSeriesId == 'jf7'), isTrue); expect(out.every((e) => e.tracked && e.seriesId == 7 && e.jellyfinSeriesId == 'jf7'), isTrue);
}); });
@@ -205,11 +203,11 @@ void main() {
final out = matchSchedule( final out = matchSchedule(
[airing(500, 2, DateTime(2026, 10, 7, 20)), airing(500, 3, DateTime(2026, 10, 14, 20))], [airing(500, 2, DateTime(2026, 10, 7, 20)), airing(500, 3, DateTime(2026, 10, 14, 20))],
schedule: const [], schedule: const [],
series: [const AnimeFlowSeries(seriesId: 9, title: 'S', anilistId: 500, followed: true)], series: [const AnimeFlowSeries(seriesId: 9, title: 'S', anilistId: 500)],
now: now, now: now,
); );
expect(out.map((e) => e.status), [ScheduleStatus.missing, ScheduleStatus.upcoming]); expect(out.map((e) => e.status), [ScheduleStatus.missing, ScheduleStatus.upcoming]);
expect(out.every((e) => e.followed && e.seriesId == 9), isTrue); expect(out.every((e) => e.seriesId == 9), isTrue);
}); });
test('progress only while downloading, reason only when failed', () { test('progress only while downloading, reason only when failed', () {
@@ -271,7 +269,7 @@ void main() {
return json(res, { return json(res, {
'items': [ 'items': [
{'season': 1, 'episode': 3, 'anilist_id': 100, 'status': 'available', 'series_id': 7, {'season': 1, 'episode': 3, 'anilist_id': 100, 'status': 'available', 'series_id': 7,
'title': 'Show', 'followed': false}, 'title': 'Show'},
], ],
}); });
case '/flow/series': case '/flow/series':
+2 -18
View File
@@ -88,7 +88,6 @@ List<ScheduleEntry> entries() => [
airing: airing(111, 'Avail Show', 3, day(0, 9)), airing: airing(111, 'Avail Show', 3, day(0, 9)),
status: ScheduleStatus.available, status: ScheduleStatus.available,
tracked: true, tracked: true,
followed: true,
seriesId: 7, seriesId: 7,
season: 1, season: 1,
jellyfinSeriesId: 'jf7', jellyfinSeriesId: 'jf7',
@@ -201,8 +200,6 @@ void main() {
}); });
case ('GET', '/search'): case ('GET', '/search'):
json(res, {'items': searchHits}); json(res, {'items': searchHits});
case ('PUT' || 'DELETE', '/series/7/follow'):
res.statusCode = 204;
case ('POST', '/requests/1/approve' || '/requests/2/reject'): case ('POST', '/requests/1/approve' || '/requests/2/reject'):
json(res, {}); json(res, {});
default: default:
@@ -298,13 +295,9 @@ void main() {
await done(tester); await done(tester);
}); });
testWidgets('filters: following, all airing, niche too, compact', (tester) async { testWidgets('filters: all airing, niche too, compact', (tester) async {
await pumpPage(tester); await pumpPage(tester);
await tester.tap(find.text('Following')); expect(find.text('Following'), findsNothing);
await tester.pump();
expect(card(111, 3), findsOneWidget);
expect(card(222, 5), findsNothing);
await tester.tap(find.text('All airing')); await tester.tap(find.text('All airing'));
await tester.pump(); await tester.pump();
expect(card(444, 1), findsOneWidget); expect(card(444, 1), findsOneWidget);
@@ -398,15 +391,6 @@ void main() {
await done(tester); await done(tester);
}); });
testWidgets('follow toggles through AnimeFlow', (tester) async {
final repo = await pumpPage(tester);
await tester.tap(find.byKey(const ValueKey('follow-111-3')));
await io(tester);
expect(server.seen.where((r) => r.method == 'DELETE' && r.uri.path.endsWith('/series/7/follow')), hasLength(1));
expect(repo.forced.length, greaterThanOrEqualTo(2)); // reloaded
await done(tester);
});
testWidgets('put it up resolves the Jellyfin episode', (tester) async { testWidgets('put it up resolves the Jellyfin episode', (tester) async {
final jf = FakeJf(); final jf = FakeJf();
final accounts = JellyfinAccounts( final accounts = JellyfinAccounts(
+2 -2
View File
@@ -33,8 +33,8 @@ off by deleting it in the same commit as the code. Wire changes also edit
should open the video track's URL alone, not the EDL. should open the video track's URL alone, not the EDL.
4. **AniList account**: sign in (OAuth via a localhost callback), "My list" 4. **AniList account**: sign in (OAuth via a localhost callback), "My list"
filter on the Schedule; today's "Following" (AnimeFlow's monitored filter on the Schedule from the user's AniList lists. This is the
shows) becomes "On the server". Schedule's personal tracking; AnimeFlow has no per-user follow.
5. **Ops**: check shaders on Windows; the relay e2e "source drops 5. **Ops**: check shaders on Windows; the relay e2e "source drops
uncleanly and resumes" fails about half the time under the full suite. uncleanly and resumes" fails about half the time under the full suite.