feat(mornye): cache motion covers and refine player and search

Save album motion videos in app storage during downloads and prefer offline playback with an aspect-aware layout. Add cache management for saved videos.

Center active lyrics on opening, expand lyrics during manual scrolling, and adjust landscape header spacing. Group search rows and skeletons with dividers and remove redundant result icons.

Validation: Flutter analysis clean; 723 tests passed, 4 skipped; iOS simulator build installed.
This commit is contained in:
zarzet committed 2026-09-19 17:26:26 +07:00
1 parent d5ac292d36
commit e61ff64adc
26 files changed
+1209 -291

No files matched your search

@@ -83,6 +83,100 @@ Future<String> _normalizeDecryptedIsoBmffAudioPath(
}
extension _DownloadQueueFinalization on DownloadQueueNotifier {
Future<void> _saveDownloadedMotionArtwork(
Ref ref,
DownloadItem item,
Track track,
Map<String, dynamic> result,
) async {
// Store independently of history preferences and the selected UI theme.
// A later switch to Mornye must still work without network access.
try {
final album = (
album:
_resolveMetadataText(
track.albumName,
result['album']?.toString(),
) ??
item.track.albumName,
artist:
_resolveMetadataText(
track.artistName,
result['artist']?.toString(),
) ??
item.track.artistName,
);
final extensions = ref.read(extensionProvider).extensions;
final preferred = ref.read(settingsProvider).searchProvider;
final providers =
<String>{
if (item.track.source?.isNotEmpty == true) item.track.source!,
?preferred,
}
.where(
(id) => extensions.any(
(extension) =>
extension.id == id &&
extension.enabled &&
extension.hasCustomSearch,
),
)
.toList();
final store = ref.read(motionArtworkStoreProvider);
final artwork = await store.save(
album,
resolveSource: () async {
final direct = normalizeRemoteHttpUrl(
item.track.headerVideoUrl ??
track.headerVideoUrl ??
result['header_video']?.toString(),
);
if (direct != null) return direct;
final source = item.track.source;
final albumId = item.track.albumId;
if (source != null &&
providers.contains(source) &&
albumId?.isNotEmpty == true) {
try {
final metadata = await PlatformBridge.getProviderMetadata(
source,
'album',
stripPrefixedResourceId(albumId!),
).timeout(const Duration(seconds: 6));
final info = metadata['album_info'];
final url = normalizeRemoteHttpUrl(
((info is Map ? info['header_video'] : null) ??
metadata['header_video'])
?.toString(),
);
if (url != null) return url;
} catch (_) {}
}
return findPlayerMotionArtwork(
album: album,
providerIds: providers,
search: (provider, query) =>
PlatformBridge.customSearchWithExtension(
provider,
query,
options: {'filter': 'album', 'limit': 8},
).timeout(const Duration(seconds: 6)),
loadAlbum: (provider, id) => PlatformBridge.getProviderMetadata(
provider,
'album',
id,
).timeout(const Duration(seconds: 6)),
);
},
);
if (artwork != null && ref.mounted) {
ref.invalidate(playerMotionArtworkProvider(album));
}
} catch (error) {
_log.w('Optional motion artwork was not saved: ${error.runtimeType}');
}
}
Future<_AutoConversionOutcome> _autoConvertDownloadedFile({
required String itemId,
required String filePath,