recover renamed media and rotate backdrops

This commit is contained in:
ngfchl
2026-07-18 21:27:49 +08:00
parent 2f9d5ab860
commit c2ea95612b
2 changed files with 104 additions and 48 deletions
+49 -47
View File
@@ -1954,6 +1954,9 @@ class _MediaDetailPanelState extends ConsumerState<_MediaDetailPanel> {
int? _loadedTMDBID;
bool _loadingTMDBDetails = false;
bool _refreshingMedia = false;
final _backdropController = PageController();
Timer? _backdropTimer;
var _backdropIndex = 0;
final _updatePopover = ShadPopoverController();
@override
@@ -1980,6 +1983,8 @@ class _MediaDetailPanelState extends ConsumerState<_MediaDetailPanel> {
@override
void dispose() {
_backdropTimer?.cancel();
_backdropController.dispose();
_updatePopover.dispose();
super.dispose();
}
@@ -2063,6 +2068,7 @@ class _MediaDetailPanelState extends ConsumerState<_MediaDetailPanel> {
_tmdbDetails = details;
_loadedTMDBID = tmdbID;
});
_restartBackdropCarousel();
}
} catch (_) {
// Artwork enrichments are optional and should not interrupt playback.
@@ -2071,6 +2077,34 @@ class _MediaDetailPanelState extends ConsumerState<_MediaDetailPanel> {
}
}
List<String> _heroBackdropPaths(MediaLibraryItem item) {
final images = _tmdbDetails?['images'];
final paths = <String>[
if (item.backdropPath?.isNotEmpty == true) item.backdropPath!,
if (_tmdbDetails?['backdrop_path']?.toString().isNotEmpty == true)
_tmdbDetails!['backdrop_path'].toString(),
if (images is Map) ..._imagePaths(images['backdrops']),
];
return paths.toSet().take(10).toList();
}
void _restartBackdropCarousel() {
_backdropTimer?.cancel();
final count = _heroBackdropPaths(widget.work.primary).length;
if (count < 2) return;
_backdropIndex = 0;
if (_backdropController.hasClients) _backdropController.jumpToPage(0);
_backdropTimer = Timer.periodic(const Duration(seconds: 8), (_) {
if (!mounted || !_backdropController.hasClients) return;
_backdropIndex = (_backdropIndex + 1) % count;
_backdropController.animateToPage(
_backdropIndex,
duration: const Duration(milliseconds: 450),
curve: Curves.easeOut,
);
});
}
@override
Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
@@ -2163,9 +2197,7 @@ class _MediaDetailPanelState extends ConsumerState<_MediaDetailPanel> {
final posterURL = item.posterPath?.isNotEmpty == true
? _tmdbImageURL(item.posterPath!, size: 'w342')
: null;
final backdropPath = item.backdropPath?.isNotEmpty == true
? item.backdropPath
: _tmdbDetails?['backdrop_path']?.toString();
final backdrops = _heroBackdropPaths(item);
return SizedBox(
height: 340,
child: ClipRRect(
@@ -2173,14 +2205,19 @@ class _MediaDetailPanelState extends ConsumerState<_MediaDetailPanel> {
child: Stack(
fit: StackFit.expand,
children: [
if (backdropPath != null && backdropPath.isNotEmpty)
CachedNetworkImage(
imageUrl: _tmdbImageURL(backdropPath, size: 'w1280'),
fit: BoxFit.cover,
errorWidget: (_, _, _) => _tmdbDirectFallback(
path: backdropPath,
size: 'w1280',
fallback: ColoredBox(color: cs.muted),
if (backdrops.isNotEmpty)
PageView.builder(
controller: _backdropController,
itemCount: backdrops.length,
onPageChanged: (index) => _backdropIndex = index,
itemBuilder: (_, index) => CachedNetworkImage(
imageUrl: _tmdbImageURL(backdrops[index], size: 'w1280'),
fit: BoxFit.cover,
errorWidget: (_, _, _) => _tmdbDirectFallback(
path: backdrops[index],
size: 'w1280',
fallback: ColoredBox(color: cs.muted),
),
),
)
else
@@ -2353,7 +2390,6 @@ class _MediaDetailPanelState extends ConsumerState<_MediaDetailPanel> {
? Map<String, dynamic>.from(details['images'] as Map)
: const <String, dynamic>{};
final posters = _imagePaths(images['posters']);
final backdrops = _imagePaths(images['backdrops']);
final credits = details['credits'] is Map
? Map<String, dynamic>.from(details['credits'] as Map)
: const <String, dynamic>{};
@@ -2364,46 +2400,12 @@ class _MediaDetailPanelState extends ConsumerState<_MediaDetailPanel> {
.take(12)
.toList() ??
const <Map<String, dynamic>>[];
if (posters.isEmpty && backdrops.isEmpty && cast.isEmpty) {
if (posters.isEmpty && cast.isEmpty) {
return const SizedBox.shrink();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (backdrops.isNotEmpty) ...[
Text(
'横幅与剧照',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: cs.foreground,
),
),
const SizedBox(height: 8),
SizedBox(
height: 155,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: backdrops.length,
separatorBuilder: (_, _) => const SizedBox(width: 8),
itemBuilder: (context, index) => ClipRRect(
borderRadius: BorderRadius.circular(6),
child: CachedNetworkImage(
imageUrl: _tmdbImageURL(backdrops[index], size: 'w780'),
width: 260,
fit: BoxFit.cover,
errorWidget: (_, _, _) => _tmdbDirectFallback(
path: backdrops[index],
size: 'w780',
width: 260,
fallback: const SizedBox(width: 260),
),
),
),
),
),
const SizedBox(height: 18),
],
if (posters.isNotEmpty) ...[
Text(
'更多海报',
+55 -1
View File
@@ -1625,8 +1625,11 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
if (current != null) {
return _cacheResolvedCloudFile(knownFile, current);
}
} catch (_) {
} catch (error) {
// A rename can replace the cloud record. Locate its new ID below.
_appendScanLog(
'[同步识别][DEBUG] 旧 File ID 查询失败:${knownFile.id}$error;开始回退定位',
);
}
final cached = await FileMetadataCache.file(knownFile.id);
@@ -1686,6 +1689,57 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
}
return _cacheResolvedCloudFile(knownFile, resolved);
}
// The user may have renamed the file outside this app, so its old name is
// no longer searchable. Resolve the previous parent directory by name and
// compare every child by GCID, which remains stable across a rename.
final gcid = knownFile.gcid?.trim();
final parentName = _parentDirectoryName(knownFile.cloudPath);
if (gcid != null && gcid.isNotEmpty && parentName != null) {
try {
_appendScanLog('[同步识别][DEBUG] 按父目录 GCID 定位:目录=$parentNamegcid=$gcid');
final foldersResponse = await _api!.searchFiles(
parentName,
page: 0,
pageSize: 100,
);
final folders = _extractFiles(
foldersResponse,
).where((file) => file.isDirectory && file.name == parentName);
for (final folder in folders) {
final response = await _api!.fsFiles(
parentID: folder.id,
page: 0,
pageSize: 1000,
orderBy: 0,
sortType: 0,
);
for (final candidate in _extractFiles(response)) {
if (candidate.isDirectory) continue;
var resolved = candidate;
if (resolved.gcid != gcid) {
try {
final detail = await _api!.fsDetail(candidate.id);
resolved =
_fileFromDetail(detail, candidate.id, candidate) ??
candidate;
} catch (_) {
continue;
}
}
if (resolved.gcid == gcid) {
_appendScanLog(
'[同步识别][DEBUG] GCID 定位成功:${knownFile.id} -> '
'${resolved.id},名称=${resolved.name}',
);
return _cacheResolvedCloudFile(knownFile, resolved);
}
}
}
} catch (_) {
// The caller emits the final diagnostic after all resolution paths fail.
}
}
return null;
}