Merge the deduplication and dead-code removal pass

Consolidates duplicated logic behind shared implementations — paginated
grid tabs, focus chrome, cached remote stores, sheet selection columns,
the server artifact store and a test fixture layer — and removes code
that had become unreachable. Net reduction of about 5,500 lines with no
behaviour change.

Where a fix had landed separately in code that moved into a shared
helper, the fix was re-applied inside the helper rather than left behind
in the copy that went away.
This commit is contained in:
edde746
2026-07-26 19:41:23 +02:00
425 changed files with 12045 additions and 17543 deletions
+106 -164
View File
@@ -62,13 +62,23 @@ class TrackersProvider extends ChangeNotifier with DisposableChangeNotifierMixin
final MalAuthService _malAuth;
final AnilistAuthService _anilistAuth;
final SimklAuthService _simklAuth;
final TrackerAccountStore _malStore = trackerAccountStore(TrackerService.mal);
final TrackerAccountStore _anilistStore = trackerAccountStore(TrackerService.anilist);
final TrackerAccountStore _simklStore = trackerAccountStore(TrackerService.simkl);
TrackerSession? _mal;
TrackerSession? _anilist;
TrackerSession? _simkl;
final _TrackerSlot _mal = _TrackerSlot(
TrackerService.mal,
(session, {required onInvalidated, onUpdated}) =>
MalTracker.instance.rebindSession(session, onSessionInvalidated: onInvalidated, onSessionUpdated: onUpdated),
);
final _TrackerSlot _anilist = _TrackerSlot(
TrackerService.anilist,
(session, {required onInvalidated, onUpdated}) =>
AnilistTracker.instance.rebindSession(session, onSessionInvalidated: onInvalidated),
);
final _TrackerSlot _simkl = _TrackerSlot(
TrackerService.simkl,
(session, {required onInvalidated, onUpdated}) =>
SimklTracker.instance.rebindSession(session, onSessionInvalidated: onInvalidated),
);
late final List<_TrackerSlot> _slots = [_mal, _anilist, _simkl];
String _activeUserUuid = '';
int _profileBindingGeneration = 0;
@@ -76,22 +86,13 @@ class TrackersProvider extends ChangeNotifier with DisposableChangeNotifierMixin
Completer<void>? _cancelCompleter;
int _connectGeneration = 0;
// Bumped on every rebind so a late callback from a disposed client (e.g. an
// in-flight MAL token refresh that resolves after a profile switch) can't
// persist or clear a session under the wrong profile, and so a disconnect
// racing an in-flight profile load only suppresses its own service. Mirrors
// TraktAccountProvider's binding-generation guard, but per service.
final _RebindGeneration _malRebind = _RebindGeneration();
final _RebindGeneration _anilistRebind = _RebindGeneration();
final _RebindGeneration _simklRebind = _RebindGeneration();
TrackerSession? get mal => _mal.session;
TrackerSession? get anilist => _anilist.session;
TrackerSession? get simkl => _simkl.session;
TrackerSession? get mal => _mal;
TrackerSession? get anilist => _anilist;
TrackerSession? get simkl => _simkl;
bool get isMalConnected => _mal != null;
bool get isAnilistConnected => _anilist != null;
bool get isSimklConnected => _simkl != null;
bool get isMalConnected => _mal.session != null;
bool get isAnilistConnected => _anilist.session != null;
bool get isSimklConnected => _simkl.session != null;
/// The live MAL client for the Explore catalog, shared with the scrobble
/// tracker so both ride one session (MAL rotates refresh tokens — a second
@@ -99,17 +100,17 @@ class TrackersProvider extends ChangeNotifier with DisposableChangeNotifierMixin
/// provider's own session so a freshly-mounted profile subtree never sees
/// the previous profile's client while its sessions are still loading;
/// every rebind is followed by a notify, so proxy consumers track identity.
MalClient? get malCatalogClient => _mal == null ? null : MalTracker.instance.client;
MalClient? get malCatalogClient => _mal.session == null ? null : MalTracker.instance.client;
/// Live AniList and Simkl clients for Explore. Like [malCatalogClient],
/// these are gated on this provider's profile-bound sessions so a fresh
/// profile subtree cannot observe clients still bound to the prior profile.
AnilistClient? get anilistCatalogClient => _anilist == null ? null : AnilistTracker.instance.client;
SimklClient? get simklCatalogClient => _simkl == null ? null : SimklTracker.instance.client;
AnilistClient? get anilistCatalogClient => _anilist.session == null ? null : AnilistTracker.instance.client;
SimklClient? get simklCatalogClient => _simkl.session == null ? null : SimklTracker.instance.client;
String? get malUsername => _mal?.username;
String? get anilistUsername => _anilist?.username;
String? get simklUsername => _simkl?.username;
String? get malUsername => _mal.session?.username;
String? get anilistUsername => _anilist.session?.username;
String? get simklUsername => _simkl.session?.username;
bool isConnecting(TrackerService service) => _connecting == service;
@@ -131,26 +132,14 @@ class TrackersProvider extends ChangeNotifier with DisposableChangeNotifierMixin
// Snapshot each service's rebind generation before the await so a disconnect
// that races this load only suppresses its own service (whose generation
// moves) rather than dropping the freshly-loaded sessions for the others.
final malRebind = _malRebind.value;
final anilistRebind = _anilistRebind.value;
final simklRebind = _simklRebind.value;
final results = await Future.wait<TrackerSession?>([
_malStore.load(userUuid),
_anilistStore.load(userUuid),
_simklStore.load(userUuid),
]);
final rebinds = [for (final slot in _slots) slot.rebindGeneration];
final results = await Future.wait<TrackerSession?>([for (final slot in _slots) slot.store.load(userUuid)]);
if (!_isCurrentProfileBinding(userUuid, generation)) return;
if (_malRebind.value == malRebind) {
_mal = results.first;
_rebindMal();
}
if (_anilistRebind.value == anilistRebind) {
_anilist = results[1];
_rebindAnilist();
}
if (_simklRebind.value == simklRebind) {
_simkl = results[2];
_rebindSimkl();
for (var i = 0; i < _slots.length; i++) {
final slot = _slots[i];
if (slot.rebindGeneration != rebinds[i]) continue;
slot.session = results[i];
_rebind(slot);
}
// Connect/disconnect may flip `needsFribb` — drop cached resolver IDs so
// the next lookup re-evaluates whether to consult Fribb.
@@ -159,78 +148,51 @@ class TrackersProvider extends ChangeNotifier with DisposableChangeNotifierMixin
}
Future<bool> connectMal({required void Function(OAuthProxyStart) onCodeReady}) => _runConnect(
service: TrackerService.mal,
alreadyConnected: isMalConnected,
_mal,
authorize: () => _malAuth.authorize(
onCodeReady: onCodeReady,
shouldCancel: () => _cancelCompleter?.isCompleted ?? false,
shouldCancel: _isConnectCancelled,
onCancel: _cancelCompleter!.future,
),
enrich: _enrichMal,
store: _malStore,
assign: (s) {
_mal = s;
_rebindMal();
},
);
Future<void> disconnectMal() => _clearAndRebind(TrackerService.mal, _malStore, () {
_mal = null;
_rebindMal();
});
Future<void> disconnectMal() => _clearAndRebind(_mal);
Future<bool> connectAnilist({required void Function(OAuthProxyStart) onCodeReady}) => _runConnect(
service: TrackerService.anilist,
alreadyConnected: isAnilistConnected,
_anilist,
authorize: () => _anilistAuth.authorize(
onCodeReady: onCodeReady,
shouldCancel: () => _cancelCompleter?.isCompleted ?? false,
shouldCancel: _isConnectCancelled,
onCancel: _cancelCompleter!.future,
),
enrich: _enrichAnilist,
store: _anilistStore,
assign: (s) {
_anilist = s;
_rebindAnilist();
},
);
Future<void> disconnectAnilist() => _clearAndRebind(TrackerService.anilist, _anilistStore, () {
_anilist = null;
_rebindAnilist();
});
Future<void> disconnectAnilist() => _clearAndRebind(_anilist);
Future<bool> connectSimkl({required void Function(DeviceCode code) onCodeReady}) => _runConnect(
service: TrackerService.simkl,
alreadyConnected: isSimklConnected,
_simkl,
authorize: () => _simklAuth.authorize(
onCodeReady: onCodeReady,
shouldCancel: () => _cancelCompleter?.isCompleted ?? false,
shouldCancel: _isConnectCancelled,
onCancel: _cancelCompleter!.future,
),
enrich: _enrichSimkl,
store: _simklStore,
assign: (s) {
_simkl = s;
_rebindSimkl();
},
);
Future<void> disconnectSimkl() => _clearAndRebind(TrackerService.simkl, _simklStore, () {
_simkl = null;
_rebindSimkl();
});
Future<void> disconnectSimkl() => _clearAndRebind(_simkl);
Future<bool> _runConnect({
required TrackerService service,
required bool alreadyConnected,
bool _isConnectCancelled() => _cancelCompleter?.isCompleted ?? false;
Future<bool> _runConnect(
_TrackerSlot slot, {
required Future<TrackerSession?> Function() authorize,
required Future<TrackerSession> Function(TrackerSession raw) enrich,
required TrackerAccountStore store,
required void Function(TrackerSession session) assign,
}) async {
if (isDisposed || _connecting != null || alreadyConnected) return false;
if (isDisposed || _connecting != null || slot.session != null) return false;
final service = slot.service;
final userUuid = _activeUserUuid;
final generation = ++_connectGeneration;
_connecting = service;
@@ -248,11 +210,12 @@ class TrackersProvider extends ChangeNotifier with DisposableChangeNotifierMixin
enrich: enrich,
save: (session) async {
if (!_isCurrentConnect(service, userUuid, generation)) return;
await store.save(userUuid, session);
await slot.store.save(userUuid, session);
},
assign: (session) {
if (!_isCurrentConnect(service, userUuid, generation)) return;
assign(session);
slot.session = session;
_rebind(slot);
TrackerCoordinator.instance.invalidateResolverCache();
assigned = true;
},
@@ -267,20 +230,17 @@ class TrackersProvider extends ChangeNotifier with DisposableChangeNotifierMixin
}
}
Future<void> _clearAndRebind(
TrackerService service,
TrackerAccountStore store,
void Function() clearAndRebind,
) async {
_invalidateConnect(service);
Future<void> _clearAndRebind(_TrackerSlot slot) async {
_invalidateConnect(slot.service);
final userUuid = _activeUserUuid;
// `clearAndRebind` bumps the affected service's rebind generation, which is
// what stops an in-flight profile load from resurrecting the cleared
// session — so we no longer touch the shared profile-binding generation
// (which would also abort that load for the other two services).
clearAndRebind();
// The rebind bumps the affected service's generation, which is what stops
// an in-flight profile load from resurrecting the cleared session — so we
// no longer touch the shared profile-binding generation (which would also
// abort that load for the other two services).
slot.session = null;
_rebind(slot);
safeNotifyListeners();
await store.clear(userUuid);
await slot.store.clear(userUuid);
}
void _invalidateConnect([TrackerService? service]) {
@@ -322,68 +282,33 @@ class TrackersProvider extends ChangeNotifier with DisposableChangeNotifierMixin
},
);
/// Snapshot the active profile + bump this service's rebind generation,
/// returning the bound uuid and an `isCurrent` predicate. Bumping here is what
/// lets a stale client callback — or a racing profile load — detect that it
/// has been superseded for this service.
(String, bool Function()) _beginRebind(_RebindGeneration gen) {
final boundUuid = _activeUserUuid;
final generation = gen.bump();
bool isCurrent() => !isDisposed && boundUuid == _activeUserUuid && generation == gen.value;
return (boundUuid, isCurrent);
}
void _rebindMal() {
/// Push a slot's session to its tracker, snapshotting the active profile and
/// bumping the slot's rebind generation first. Bumping here is what lets a
/// stale client callback — or a racing profile load — detect that it has been
/// superseded for this service.
void _rebind(_TrackerSlot slot) {
if (isDisposed) return;
final (boundUuid, isCurrent) = _beginRebind(_malRebind);
MalTracker.instance.rebindSession(
_mal,
onSessionInvalidated: () {
if (isCurrent()) _handleInvalidated(_malStore, boundUuid, () => _mal = null, _rebindMal);
},
onSessionUpdated: (next) {
final boundUuid = _activeUserUuid;
final generation = ++slot.rebindGeneration;
bool isCurrent() => !isDisposed && boundUuid == _activeUserUuid && generation == slot.rebindGeneration;
slot.bind(
slot.session,
onInvalidated: () {
if (!isCurrent()) return;
_mal = next;
_malStore.save(boundUuid, next);
slot.store.clear(boundUuid);
slot.session = null;
_rebind(slot);
safeNotifyListeners();
},
onUpdated: (next) {
if (!isCurrent()) return;
slot.session = next;
slot.store.save(boundUuid, next);
safeNotifyListeners();
},
);
}
void _rebindAnilist() {
if (isDisposed) return;
final (boundUuid, isCurrent) = _beginRebind(_anilistRebind);
AnilistTracker.instance.rebindSession(
_anilist,
onSessionInvalidated: () {
if (isCurrent()) _handleInvalidated(_anilistStore, boundUuid, () => _anilist = null, _rebindAnilist);
},
);
}
void _rebindSimkl() {
if (isDisposed) return;
final (boundUuid, isCurrent) = _beginRebind(_simklRebind);
SimklTracker.instance.rebindSession(
_simkl,
onSessionInvalidated: () {
if (isCurrent()) _handleInvalidated(_simklStore, boundUuid, () => _simkl = null, _rebindSimkl);
},
);
}
void _handleInvalidated(
TrackerAccountStore store,
String userUuid,
void Function() clearSession,
void Function() rebind,
) {
store.clear(userUuid);
clearSession();
rebind();
safeNotifyListeners();
}
@override
void dispose() {
_invalidateConnect();
@@ -394,12 +319,29 @@ class TrackersProvider extends ChangeNotifier with DisposableChangeNotifierMixin
}
}
/// A monotonic per-service rebind counter. Each rebind bumps it so a stale
/// client callback — or a profile load that started earlier — can tell it has
/// been superseded for that service.
class _RebindGeneration {
int _value = 0;
/// Pushes a session to one service's tracker singleton. `onUpdated` is only
/// wired for MAL, the one service that rotates its refresh token.
typedef _TrackerBind =
void Function(
TrackerSession? session, {
required void Function() onInvalidated,
void Function(TrackerSession session)? onUpdated,
});
int bump() => ++_value;
int get value => _value;
/// Owns one service's session, the generation guarding its rebinds, and the
/// adapter that pushes that session to the service's tracker singleton.
class _TrackerSlot {
_TrackerSlot(this.service, this.bind) : store = trackerAccountStore(service);
final TrackerService service;
final TrackerAccountStore store;
final _TrackerBind bind;
TrackerSession? session;
/// Bumped on every rebind so a late callback from a disposed client (e.g. an
/// in-flight MAL token refresh that resolves after a profile switch) can't
/// persist or clear a session under the wrong profile, and so a disconnect
/// racing an in-flight profile load only suppresses its own service. Mirrors
/// TraktAccountProvider's binding-generation guard, but per service.
int rebindGeneration = 0;
}