diff --git a/lib/base/audio_handler.dart b/lib/base/audio_handler.dart index e6702bbd..daf60a83 100644 --- a/lib/base/audio_handler.dart +++ b/lib/base/audio_handler.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:audio_service/audio_service.dart'; import 'package:basic_audio_handler/basic_audio_handler.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:get/get_rx/src/rx_types/rx_types.dart'; import 'package:get/get_utils/src/extensions/num_extensions.dart'; @@ -184,8 +183,6 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { // ============================================================================================== // ================================== QueueManager Overriden ==================================== - Color? latestExtractedColor; - @override void onIndexChanged(int newIndex, Q newItem) async { refreshNotification(newItem); @@ -196,16 +193,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { }, youtubeID: (finalItem) async { settings.player.save(lastPlayedIndices: {LibraryCategory.youtube: newIndex}); - final image = await ThumbnailManager.inst.getYoutubeThumbnailAndCache(id: finalItem.id); - if (image != null && finalItem == currentItem) { - // -- only extract if same item is still playing, i.e. user didn't skip. - final color = await CurrentColor.inst.extractPaletteFromImage(image.path, paletteSaveDirectory: Directory(AppDirs.YT_PALETTES), useIsolate: true); - if (color != null && finalItem == currentItem) { - // -- only update if same item is still playing, i.e. user didn't skip. - CurrentColor.inst.updatePlayerColorFromColor(color.color); - latestExtractedColor = color.color; - } - } + await CurrentColor.inst.updatePlayerColorFromYoutubeID(finalItem); }, ); } @@ -485,7 +473,13 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { final initialVideo = await VideoController.inst.updateCurrentVideo(tr, returnEarly: true); // -- generating artwork in case it wasnt, to be displayed in notification - Indexer.inst.getArtwork(imagePath: tr.pathToImage, compressed: false).then((value) => refreshNotification()); + File(tr.pathToImage).exists().then((exists) { + // -- we check if it exists to avoid refreshing notification redundently. + // -- otherwise `getArtwork` already handles duplications. + if (!exists) { + Indexer.inst.getArtwork(imagePath: tr.pathToImage, compressed: false, checkFileFirst: false).then((value) => refreshNotification()); + } + }); Future setPls() async { final dur = await setSource( diff --git a/lib/controller/current_color.dart b/lib/controller/current_color.dart index 6eaac0a6..918167d0 100644 --- a/lib/controller/current_color.dart +++ b/lib/controller/current_color.dart @@ -3,20 +3,21 @@ import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; - import 'package:get/get.dart'; -import 'package:queue/queue.dart' as qs; import 'package:palette_generator/palette_generator.dart'; -import 'package:dynamic_color/dynamic_color.dart'; +import 'package:queue/queue.dart' as qs; import 'package:namida/class/color_m.dart'; import 'package:namida/class/track.dart'; import 'package:namida/controller/indexer_controller.dart'; import 'package:namida/controller/player_controller.dart'; import 'package:namida/controller/settings_controller.dart'; +import 'package:namida/controller/thumbnail_manager.dart'; import 'package:namida/core/constants.dart'; import 'package:namida/core/extensions.dart'; +import 'package:namida/youtube/class/youtube_id.dart'; Color get playerStaticColor => Get.isDarkMode ? playerStaticColorDark : playerStaticColorLight; Color get playerStaticColorLight => Color(settings.staticColor.value); @@ -55,7 +56,8 @@ class CurrentColor { final isGeneratingAllColorPalettes = false.obs; - final colorsMap = {}; + final _colorsMap = {}; + final _colorsMapYTID = {}; Timer? _colorsSwitchTimer; void switchColorPalettes(bool isPlaying) { @@ -140,11 +142,55 @@ class CurrentColor { } Future updatePlayerColorFromTrack(Selectable? track, int? index, {bool updateIndexOnly = false}) async { - if (!updateIndexOnly && track != null && (settings.autoColor.value || settings.forceMiniplayerTrackColor.value)) { + if (!updateIndexOnly && track != null) { + await _updatePlayerColorFromItem( + getColorPalette: () async => await getTrackColors(track.track), + stillPlaying: () => track.track == Player.inst.nowPlayingTrack, + ); + } + if (track != null) { + currentPlayingTrack.value = null; // nullifying to re-assign safely if subtype has changed + currentPlayingTrack.value = track; + } + if (index != null) { + currentPlayingIndex.value = index; + } + } + + Future updatePlayerColorFromYoutubeID(YoutubeID ytIdItem) async { + final id = ytIdItem.id; + if (id == '') return; + + // -- only extract if same item is still playing, i.e. user didn't skip. + bool stillPlaying() => ytIdItem.id == Player.inst.nowPlayingVideoID?.id; + + await _updatePlayerColorFromItem( + getColorPalette: () async { + if (_colorsMapYTID[id] != null) return _colorsMapYTID[id]!; + + final image = await ThumbnailManager.inst.getYoutubeThumbnailAndCache(id: id); + if (image != null && stillPlaying()) { + final color = await CurrentColor.inst.extractPaletteFromImage(image.path, paletteSaveDirectory: Directory(AppDirs.YT_PALETTES), useIsolate: true); + if (color != null && stillPlaying()) { + _colorsMapYTID[id] = color; // saving in memory + return color; + } + } + return null; + }, + stillPlaying: stillPlaying, + ); + } + + Future _updatePlayerColorFromItem({ + required Future Function() getColorPalette, + required bool Function() stillPlaying, + }) async { + if (settings.autoColor.value || settings.forceMiniplayerTrackColor.value) { NamidaColor? namidaColor; - final trColors = await getTrackColors(track.track); - if (track.track != Player.inst.nowPlayingTrack) return; // -- check current track + final trColors = await getColorPalette(); + if (trColors == null || !stillPlaying()) return; // -- check current item _namidaColorMiniplayer.value = trColors.color; if (settings.autoColor.value) { @@ -161,13 +207,6 @@ class CurrentColor { } } } - if (track != null) { - currentPlayingTrack.value = null; // nullifying to re-assign safely if subtype has changed - currentPlayingTrack.value = track; - } - if (index != null) { - currentPlayingIndex.value = index; - } } void resetCurrentPlayingTrack() { @@ -205,7 +244,7 @@ class CurrentColor { final filename = settings.groupArtworksByAlbum.value ? track.albumIdentifier : track.filename; - final valInMap = colorsMap[filename]; + final valInMap = _colorsMap[filename]; if (!forceReCheck && valInMap != null) { return maybeDelightned(valInMap); } @@ -384,7 +423,7 @@ class CurrentColor { } void _updateInColorMap(String filenameWoExt, NamidaColor? nc) { - if (nc != null) colorsMap[filenameWoExt] = nc; + if (nc != null) _colorsMap[filenameWoExt] = nc; if (filenameWoExt == Player.inst.nowPlayingTrack.path.getFilename) { updatePlayerColorFromTrack(Player.inst.nowPlayingTrack, null); } diff --git a/lib/controller/player_controller.dart b/lib/controller/player_controller.dart index 61876489..2a0e5f19 100644 --- a/lib/controller/player_controller.dart +++ b/lib/controller/player_controller.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'package:audio_service/audio_service.dart'; -import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:just_audio/just_audio.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; @@ -100,8 +99,6 @@ class Player { int get sleepingTrackIndex => sleepAfterTracks + currentIndex - 1; - Color? get latestExtractedColor => _audioHandler.latestExtractedColor; - // -- error playing track void cancelPlayErrorSkipTimer() => _audioHandler.cancelPlayErrorSkipTimer(); int get playErrorRemainingSecondsToSkip => _audioHandler.playErrorRemainingSecondsToSkip; diff --git a/lib/controller/thumbnail_manager.dart b/lib/controller/thumbnail_manager.dart index 7b56e786..3d1b346b 100644 --- a/lib/controller/thumbnail_manager.dart +++ b/lib/controller/thumbnail_manager.dart @@ -109,15 +109,13 @@ class ThumbnailManager { String? channelUrlOrID, bool isImportantInCache = true, FutureOr Function()? beforeFetchingFromInternet, + void Function(Uint8List? bytes)? bytesIfWontWriteToFile, bool hqChannelImage = false, }) async { if (id == null && channelUrlOrID == null) return null; - void trySavingLastAccessed(File? file) async { - final time = isImportantInCache ? DateTime.now() : DateTime(1970); - try { - if (file != null && await file.exists()) await file.setLastAccessed(time); - } catch (_) {} + void updateLastAccessed(File? file) async { + await file?.setLastAccessed(DateTime.now()).catchError((_) {}); } final file = imageUrlToCacheFile(id: id, url: channelUrlOrID); @@ -125,7 +123,7 @@ class ThumbnailManager { if (file.existsSync() == true) { _printie('Downloading Thumbnail Already Exists'); - trySavingLastAccessed(file); + if (isImportantInCache) updateLastAccessed(file); return file; } @@ -137,8 +135,16 @@ class ThumbnailManager { if (res != null) channelUrlOrID = res; } - final bytes = await getYoutubeThumbnailAsBytes(youtubeId: id, url: channelUrlOrID, keepInMemory: false); + final bytes = await getYoutubeThumbnailAsBytes( + youtubeId: id, + url: channelUrlOrID, + keepInMemory: isImportantInCache ? false : true, // important in cache will write to file so we dont need to keep in memory + ); _printie('Downloading Thumbnail Finished with ${bytes?.length} bytes'); + if (!isImportantInCache) { + bytesIfWontWriteToFile?.call(bytes); + return null; + } final savedFileFuture = id != null ? saveThumbnailToStorage( @@ -153,8 +159,9 @@ class ThumbnailManager { bytes: bytes, ); - savedFileFuture.then(trySavingLastAccessed); - return savedFileFuture; + final savedFile = await savedFileFuture; + updateLastAccessed(savedFile); + return savedFile; } void closeThumbnailClients(List links) { diff --git a/lib/packages/miniplayer.dart b/lib/packages/miniplayer.dart index 390acbff..499faa31 100644 --- a/lib/packages/miniplayer.dart +++ b/lib/packages/miniplayer.dart @@ -516,15 +516,13 @@ class NamidaMiniPlayerYoutubeID extends StatelessWidget { localVideos: YoutubeController.inst.currentCachedQualities, streamVideos: YoutubeController.inst.currentYTQualities, onLocalVideoTap: (item, video) async { - if (!Player.inst.videoInitialized) { - Player.inst.onItemPlayYoutubeIDSetQuality( - stream: null, - cachedFile: File(video.path), - videoItem: video, - useCache: true, - videoId: Player.inst.nowPlayingVideoID?.id ?? '', - ); - } + Player.inst.onItemPlayYoutubeIDSetQuality( + stream: null, + cachedFile: File(video.path), + videoItem: video, + useCache: true, + videoId: Player.inst.nowPlayingVideoID?.id ?? '', + ); }, onStreamVideoTap: (item, videoId, stream, cacheFile) async { Player.inst.onItemPlayYoutubeIDSetQuality( @@ -745,15 +743,15 @@ class _YoutubeIDImage extends StatelessWidget { @override Widget build(BuildContext context) { + final width = context.width; return YoutubeThumbnail( key: Key(video.id), videoId: video.id, - width: context.width, - height: context.width * 9 / 16, + width: width, + height: settings.forceSquaredTrackThumbnail.value ? width : width * 9 / 16, isImportantInCache: true, compressed: false, preferLowerRes: false, - forceSquared: settings.forceSquaredTrackThumbnail.value, borderRadius: 6.0 + 10.0 * cp, boxShadow: [ BoxShadow( diff --git a/lib/ui/widgets/settings/playback_settings.dart b/lib/ui/widgets/settings/playback_settings.dart index b72a05d8..5c030122 100644 --- a/lib/ui/widgets/settings/playback_settings.dart +++ b/lib/ui/widgets/settings/playback_settings.dart @@ -308,24 +308,25 @@ class PlaybackSettings extends SettingSubpageProvider { ), ), ), - getItemWrapper( - key: _PlaybackSettingsKeys.displayArtworkOnLockscreen, - child: Obx( - () => CustomSwitchListTile( - bgColor: getBgColor(_PlaybackSettingsKeys.displayArtworkOnLockscreen), - title: lang.DISPLAY_ARTWORK_ON_LOCKSCREEN, - leading: const StackedIcon( - baseIcon: Broken.gallery, - secondaryIcon: Broken.lock_circle, + if (kSdkVersion < 33) + getItemWrapper( + key: _PlaybackSettingsKeys.displayArtworkOnLockscreen, + child: Obx( + () => CustomSwitchListTile( + bgColor: getBgColor(_PlaybackSettingsKeys.displayArtworkOnLockscreen), + title: lang.DISPLAY_ARTWORK_ON_LOCKSCREEN, + leading: const StackedIcon( + baseIcon: Broken.gallery, + secondaryIcon: Broken.lock_circle, + ), + value: settings.player.lockscreenArtwork.value, + onChanged: (val) { + settings.player.save(lockscreenArtwork: !val); + AudioService.setLockScreenArtwork(!val).then((_) => Player.inst.refreshNotification()); + }, ), - value: settings.player.lockscreenArtwork.value, - onChanged: (val) { - settings.player.save(lockscreenArtwork: !val); - AudioService.setLockScreenArtwork(!val); - }, ), ), - ), getItemWrapper( key: _PlaybackSettingsKeys.killPlayerAfterDismissing, child: Obx( diff --git a/lib/ui/widgets/settings/theme_settings.dart b/lib/ui/widgets/settings/theme_settings.dart index 63192cf2..8edd48f6 100644 --- a/lib/ui/widgets/settings/theme_settings.dart +++ b/lib/ui/widgets/settings/theme_settings.dart @@ -52,9 +52,9 @@ class ThemeSetting extends SettingSubpageProvider { _ThemeSettingsKeys.language: [lang.LANGUAGE], }; - Future _refreshColorCurrentTrack() async { - if (Player.inst.currentQueueYoutube.isNotEmpty && Player.inst.latestExtractedColor != null) { - CurrentColor.inst.updatePlayerColorFromColor(Player.inst.latestExtractedColor!); + Future _refreshColorCurrentPlayingItem() async { + if (Player.inst.nowPlayingVideoID != null) { + await CurrentColor.inst.updatePlayerColorFromYoutubeID(Player.inst.nowPlayingVideoID!); } else { await CurrentColor.inst.updatePlayerColorFromTrack(Player.inst.nowPlayingTWD, null); } @@ -208,7 +208,7 @@ class ThemeSetting extends SettingSubpageProvider { if (isTrue) { CurrentColor.inst.updatePlayerColorFromColor(playerStaticColor); } else { - await _refreshColorCurrentTrack(); + await _refreshColorCurrentPlayingItem(); } }, ), @@ -228,7 +228,7 @@ class ThemeSetting extends SettingSubpageProvider { onChanged: (isTrue) async { settings.save(pickColorsFromDeviceWallpaper: !isTrue); - await _refreshColorCurrentTrack(); + await _refreshColorCurrentPlayingItem(); }, ), ), @@ -244,7 +244,7 @@ class ThemeSetting extends SettingSubpageProvider { value: settings.forceMiniplayerTrackColor.value, onChanged: (isTrue) async { settings.save(forceMiniplayerTrackColor: !isTrue); - await _refreshColorCurrentTrack(); + await _refreshColorCurrentPlayingItem(); }, ), ), @@ -260,7 +260,7 @@ class ThemeSetting extends SettingSubpageProvider { value: settings.pitchBlack.value, onChanged: (isTrue) async { settings.save(pitchBlack: !isTrue); - await _refreshColorCurrentTrack(); + await _refreshColorCurrentPlayingItem(); }, ), ), diff --git a/lib/ui/widgets/video_widget.dart b/lib/ui/widgets/video_widget.dart index 8ab08b07..1dd58110 100644 --- a/lib/ui/widgets/video_widget.dart +++ b/lib/ui/widgets/video_widget.dart @@ -1003,7 +1003,7 @@ class NamidaVideoControlsState extends State with TickerPro title: element.resolution ?? '', subtitle: sizeInBytes == null ? '' : " • ${sizeInBytes.fileSizeFormatted}", onPlay: (isSelected) { - if (!isSelected) { + if (!isSelected || !Player.inst.videoInitialized) { Player.inst.onItemPlayYoutubeIDSetQuality( stream: element, cachedFile: null, diff --git a/lib/youtube/widgets/yt_thumbnail.dart b/lib/youtube/widgets/yt_thumbnail.dart index 001b487e..72b8f9d2 100644 --- a/lib/youtube/widgets/yt_thumbnail.dart +++ b/lib/youtube/widgets/yt_thumbnail.dart @@ -40,7 +40,6 @@ class YoutubeThumbnail extends StatefulWidget { final String channelIDForHQImage; final bool hqChannelImage; final bool isPlaylist; - final bool forceSquared; final double? iconSize; final List? boxShadow; @@ -68,7 +67,6 @@ class YoutubeThumbnail extends StatefulWidget { this.channelIDForHQImage = '', this.hqChannelImage = false, this.isPlaylist = false, - this.forceSquared = true, this.iconSize, this.boxShadow, }); @@ -143,6 +141,9 @@ class _YoutubeThumbnailState extends State with LoadingItemsDe if (mounted) setState(() => imageBytes = lowerRes); } }, + bytesIfWontWriteToFile: (bytes) { + if (mounted) setState(() => imageBytes = bytes); + }, ); } @@ -198,7 +199,7 @@ class _YoutubeThumbnailState extends State with LoadingItemsDe ? Broken.user : Broken.video, iconSize: widget.iconSize ?? (widget.channelUrl != null ? null : widget.width * 0.3), - forceSquared: widget.forceSquared, + forceSquared: true, // cacheHeight: (widget.height?.round() ?? widget.width.round()) ~/ 1.2, onTopWidgets: [ ...widget.onTopWidgets,