Initial commit
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_riverpod/legacy.dart';
|
||||
|
||||
import '../core/contracts/trakt/trakt_calendar_entry.dart';
|
||||
import '../core/domain/ports/tmdb_gateway.dart';
|
||||
import '../shared/mappers/tmdb_image_url.dart';
|
||||
import '../shared/utils/app_logger.dart';
|
||||
import 'di_providers.dart';
|
||||
import 'sm_account_provider.dart';
|
||||
import 'tmdb_settings_provider.dart';
|
||||
|
||||
const _tag = 'TraktCalendar';
|
||||
|
||||
enum TraktCalendarFilter { all, shows, movies }
|
||||
|
||||
final traktCalendarFilterProvider = StateProvider<TraktCalendarFilter>(
|
||||
(ref) => TraktCalendarFilter.all,
|
||||
);
|
||||
|
||||
|
||||
final traktCalendarProvider = FutureProvider<List<TraktCalendarEntry>>((
|
||||
ref,
|
||||
) async {
|
||||
final authed = ref.read(authedTraktGatewayProvider);
|
||||
if (!await authed.isConnected()) return const [];
|
||||
|
||||
final filter = ref.watch(traktCalendarFilterProvider);
|
||||
|
||||
final entries = <TraktCalendarEntry>[];
|
||||
|
||||
if (filter != TraktCalendarFilter.movies) {
|
||||
try {
|
||||
final raw = await authed.fetchCalendarShows();
|
||||
entries.addAll(
|
||||
raw
|
||||
.map(TraktCalendarEntry.fromShowJson)
|
||||
.whereType<TraktCalendarEntry>(),
|
||||
);
|
||||
} catch (e) {
|
||||
AppLogger.warn(_tag, 'fetchCalendarShows failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (filter != TraktCalendarFilter.shows) {
|
||||
try {
|
||||
final raw = await authed.fetchCalendarMovies();
|
||||
entries.addAll(
|
||||
raw
|
||||
.map(TraktCalendarEntry.fromMovieJson)
|
||||
.whereType<TraktCalendarEntry>(),
|
||||
);
|
||||
} catch (e) {
|
||||
AppLogger.warn(_tag, 'fetchCalendarMovies failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
entries.sort((a, b) => a.firstAired.compareTo(b.firstAired));
|
||||
if (entries.isEmpty) return const [];
|
||||
|
||||
final tmdbConfig = await _resolveTmdbConfig(ref);
|
||||
if (tmdbConfig == null) return entries;
|
||||
|
||||
final gateway = ref.read(tmdbGatewayProvider);
|
||||
final cache = <String, _TmdbPosterResult>{};
|
||||
const enrichConcurrency = 5;
|
||||
final enriched = <TraktCalendarEntry>[];
|
||||
|
||||
for (var i = 0; i < entries.length; i += enrichConcurrency) {
|
||||
final chunk = entries.skip(i).take(enrichConcurrency);
|
||||
final results = await Future.wait(
|
||||
chunk.map((entry) async {
|
||||
final id = entry.tmdbId;
|
||||
if (id == null || id <= 0) return entry;
|
||||
|
||||
final cacheKey = '${entry.mediaType}:$id';
|
||||
try {
|
||||
final cached = cache[cacheKey] ??= await _fetchPoster(
|
||||
gateway,
|
||||
tmdbConfig.config,
|
||||
tmdbConfig.language,
|
||||
id.toString(),
|
||||
entry.mediaType,
|
||||
);
|
||||
var result = entry;
|
||||
if (cached.posterUrl != null) {
|
||||
result = result.withPoster(cached.posterUrl);
|
||||
}
|
||||
if (cached.title != null && cached.title!.isNotEmpty) {
|
||||
result = result.withTitle(cached.title!);
|
||||
}
|
||||
return result;
|
||||
} catch (_) {
|
||||
return entry;
|
||||
}
|
||||
}),
|
||||
);
|
||||
enriched.addAll(results);
|
||||
}
|
||||
|
||||
return enriched;
|
||||
});
|
||||
|
||||
|
||||
Future<_TmdbReadyConfig?> _resolveTmdbConfig(Ref ref) async {
|
||||
final settings = await ref.watch(tmdbSettingsProvider.future);
|
||||
if (!settings.canRequest) return null;
|
||||
|
||||
if (settings.accessMode == TmdbAccessMode.smAccount) {
|
||||
final account = await ref.watch(smAccountProvider.future);
|
||||
if (!account.canUseDataPlane) return null;
|
||||
final notifier = ref.read(smAccountProvider.notifier);
|
||||
final token = await notifier.getAccessToken();
|
||||
if (token == null) return null;
|
||||
return _TmdbReadyConfig(
|
||||
config: TmdbRequestConfig(
|
||||
apiBaseUrl: account.tmdbApiBaseUrl,
|
||||
imageBaseUrl: TmdbSettingsState.officialImageBaseUrl,
|
||||
accessToken: token,
|
||||
apiKey: '',
|
||||
onUnauthorized: () => notifier.getAccessToken(force: true),
|
||||
),
|
||||
language: settings.language,
|
||||
);
|
||||
}
|
||||
|
||||
return _TmdbReadyConfig(
|
||||
config: TmdbRequestConfig(
|
||||
apiBaseUrl: settings.effectiveApiBaseUrl,
|
||||
imageBaseUrl: settings.effectiveImageBaseUrl,
|
||||
accessToken: settings.effectiveAccessToken,
|
||||
apiKey: settings.effectiveApiKey,
|
||||
),
|
||||
language: settings.language,
|
||||
);
|
||||
}
|
||||
|
||||
Future<_TmdbPosterResult> _fetchPoster(
|
||||
TmdbGateway gateway,
|
||||
TmdbRequestConfig config,
|
||||
String language,
|
||||
String tmdbId,
|
||||
String mediaType,
|
||||
) async {
|
||||
final detail = await gateway.getMediaDetail(
|
||||
config,
|
||||
tmdbId,
|
||||
mediaType,
|
||||
language: language,
|
||||
);
|
||||
final posterUrl = TmdbImageUrl.poster(
|
||||
config.imageBaseUrl,
|
||||
detail?.posterPath,
|
||||
size: 'w342',
|
||||
);
|
||||
return _TmdbPosterResult(posterUrl: posterUrl, title: detail?.title);
|
||||
}
|
||||
|
||||
class _TmdbReadyConfig {
|
||||
final TmdbRequestConfig config;
|
||||
final String language;
|
||||
const _TmdbReadyConfig({required this.config, required this.language});
|
||||
}
|
||||
|
||||
class _TmdbPosterResult {
|
||||
final String? posterUrl;
|
||||
final String? title;
|
||||
const _TmdbPosterResult({this.posterUrl, this.title});
|
||||
}
|
||||
|
||||
|
||||
List<CalendarDateGroup> groupByDate(List<TraktCalendarEntry> entries) {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final groups = <String, List<TraktCalendarEntry>>{};
|
||||
final groupOrder = <String, DateTime>{};
|
||||
|
||||
for (final e in entries) {
|
||||
final local = e.firstAired.toLocal();
|
||||
final date = DateTime(local.year, local.month, local.day);
|
||||
final label = _dateLabel(date, today);
|
||||
(groups[label] ??= []).add(e);
|
||||
groupOrder.putIfAbsent(label, () => date);
|
||||
}
|
||||
|
||||
final sorted = groups.entries.toList()
|
||||
..sort((a, b) => groupOrder[a.key]!.compareTo(groupOrder[b.key]!));
|
||||
|
||||
return [
|
||||
for (final e in sorted) CalendarDateGroup(label: e.key, entries: e.value),
|
||||
];
|
||||
}
|
||||
|
||||
String _dateLabel(DateTime date, DateTime today) {
|
||||
final diff = date.difference(today).inDays;
|
||||
if (diff == 0) return '今天';
|
||||
if (diff == 1) return '明天';
|
||||
final weekday = _weekdayName(date.weekday);
|
||||
return '$weekday ${date.month}/${date.day}';
|
||||
}
|
||||
|
||||
String _weekdayName(int weekday) => switch (weekday) {
|
||||
1 => '周一',
|
||||
2 => '周二',
|
||||
3 => '周三',
|
||||
4 => '周四',
|
||||
5 => '周五',
|
||||
6 => '周六',
|
||||
_ => '周日',
|
||||
};
|
||||
|
||||
class CalendarDateGroup {
|
||||
final String label;
|
||||
final List<TraktCalendarEntry> entries;
|
||||
|
||||
const CalendarDateGroup({required this.label, required this.entries});
|
||||
}
|
||||
Reference in New Issue
Block a user