diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..45e27836 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,36 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "flutter", + "command": "flutter", + "args": ["build", "apk"], + "group": { + "kind": "build", + "isDefault": false + }, + "problemMatcher": [], + "label": "flutter: flutter build apk", + "detail": "" + }, + { + "type": "flutter", + "command": "flutter", + "args": [ + "build", + "apk", + "--target-platform", + "android-arm64", + "--release", + "-v" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [], + "label": "build flutter apk arm64", + "detail": "" + } + ] +} diff --git a/analysis_options.yaml b/analysis_options.yaml index 61b6c4de..c9bb0bbc 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,6 +7,15 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. +analyzer: + plugins: + - custom_lint + errors: + unnecessary_this: ignore + use_string_in_part_of_directives: ignore + +custom_lint: + include: package:flutter_lints/flutter.yaml linter: @@ -24,6 +33,5 @@ linter: rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/android/app/build.gradle b/android/app/build.gradle index ed1cf794..3345752b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -27,10 +28,6 @@ if (flutterVersionName == null) { // keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) // } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { compileSdkVersion 34 ndkVersion flutter.ndkVersion @@ -130,7 +127,6 @@ repositories { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'net.jthink:jaudiotagger:3.0.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.3' diff --git a/android/build.gradle b/android/build.gradle index 2983226c..8e4c846d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,17 +1,3 @@ -buildscript { - ext.kotlin_version = '1.8.22' - repositories { - google() - mavenCentral() - maven { url 'https://jitpack.io' } - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.4.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() @@ -24,8 +10,6 @@ allprojects { rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { project.evaluationDependsOn(':app') } diff --git a/android/settings.gradle b/android/settings.gradle index 44e62bcf..3a0f5ead 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,11 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.4.0" apply false + id "org.jetbrains.kotlin.android" version "1.8.22" apply false +} + +include ":app" \ No newline at end of file diff --git a/lib/base/audio_handler.dart b/lib/base/audio_handler.dart index 4400c435..7ca0a861 100644 --- a/lib/base/audio_handler.dart +++ b/lib/base/audio_handler.dart @@ -4,9 +4,8 @@ import 'dart:io'; import 'package:audio_service/audio_service.dart'; import 'package:basic_audio_handler/basic_audio_handler.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'; import 'package:just_audio/just_audio.dart'; +import 'package:namida/core/utils.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:playlist_manager/module/playlist_id.dart'; @@ -22,7 +21,6 @@ import 'package:namida/controller/indexer_controller.dart'; import 'package:namida/controller/lyrics_controller.dart'; import 'package:namida/controller/miniplayer_controller.dart'; import 'package:namida/controller/navigator_controller.dart'; -import 'package:namida/controller/player_controller.dart'; import 'package:namida/controller/playlist_controller.dart'; import 'package:namida/controller/queue_controller.dart'; import 'package:namida/controller/scroll_search_controller.dart'; @@ -54,7 +52,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { late final equalizer = AndroidEqualizer(); late final loudnessEnhancer = AndroidLoudnessEnhancer(); - Duration? get currentItemDuration => _currentItemDuration.value; + RxBaseCore get currentItemDuration => _currentItemDuration; final _currentItemDuration = Rxn(); Timer? _resourcesDisposeTimer; @@ -80,11 +78,6 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { var audioCacheMap = >{}; - Selectable get currentTrack => (currentItem is Selectable ? currentItem as Selectable : null) ?? kDummyTrack; - YoutubeID? get currentVideo => currentItem is YoutubeID ? currentItem as YoutubeID : null; - List get currentQueueSelectable => currentQueue.firstOrNull is Selectable ? currentQueue.cast() : []; - List get currentQueueYoutubeID => currentQueue.firstOrNull is YoutubeID ? currentQueue.cast() : []; - final currentVideoInfo = Rxn(); final currentChannelInfo = Rxn(); final currentVideoStream = Rxn(); @@ -95,10 +88,9 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { final _allowSwitchingVideoStreamIfCachedPlaying = false; - bool get isFetchingInfo => _isFetchingInfo.value; - final _isFetchingInfo = false.obs; + final isFetchingInfo = false.obs; - bool get isAudioOnlyPlayback => settings.ytIsAudioOnlyMode.value; + bool get _isAudioOnlyPlayback => settings.ytIsAudioOnlyMode.value; bool get isCurrentAudioFromCache => _isCurrentAudioFromCache; bool _isCurrentAudioFromCache = false; @@ -158,13 +150,13 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { // ================================================================================= void refreshNotification([Q? item, VideoInfo? videoInfo]) { - final exectuteOn = item ?? currentItem; + final exectuteOn = item ?? currentItem.value; exectuteOn?._execute( selectable: (finalItem) { - _notificationUpdateItem(item: exectuteOn, isItemFavourite: finalItem.track.isFavourite, itemIndex: currentIndex); + _notificationUpdateItem(item: exectuteOn, isItemFavourite: finalItem.track.isFavourite, itemIndex: currentIndex.value); }, youtubeID: (finalItem) { - _notificationUpdateItem(item: exectuteOn, isItemFavourite: false, itemIndex: currentIndex, videoInfo: videoInfo); + _notificationUpdateItem(item: exectuteOn, isItemFavourite: false, itemIndex: currentIndex.value, videoInfo: videoInfo); }, ); } @@ -172,14 +164,14 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { void _notificationUpdateItem({required Q item, required bool isItemFavourite, required int itemIndex, VideoInfo? videoInfo}) { item._execute( selectable: (finalItem) async { - mediaItem.add(finalItem.toMediaItem(currentIndex, currentQueue.length)); - playbackState.add(transformEvent(PlaybackEvent(currentIndex: currentIndex), isItemFavourite, itemIndex)); + mediaItem.add(finalItem.toMediaItem(currentIndex.value, currentQueue.length)); + playbackState.add(transformEvent(PlaybackEvent(currentIndex: currentIndex.value), isItemFavourite, itemIndex)); }, youtubeID: (finalItem) async { final info = videoInfo ?? YoutubeController.inst.getVideoInfo(finalItem.id); final thumbnail = finalItem.getThumbnailSync(); - mediaItem.add(finalItem.toMediaItem(info, thumbnail, currentIndex, currentQueue.length)); - playbackState.add(transformEvent(PlaybackEvent(currentIndex: currentIndex), isItemFavourite, itemIndex)); + mediaItem.add(finalItem.toMediaItem(info, thumbnail, currentIndex.value, currentQueue.length)); + playbackState.add(transformEvent(PlaybackEvent(currentIndex: currentIndex.value), isItemFavourite, itemIndex)); }, ); } @@ -219,8 +211,8 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { QueueController.inst.emptyLatestQueue(), ].execute(); } else { - refreshNotification(currentItem); - await QueueController.inst.updateLatestQueue(currentQueue); + refreshNotification(currentItem.value); + await QueueController.inst.updateLatestQueue(currentQueue.value); } } @@ -241,7 +233,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { youtubeID: (finalItem) {}, ); - await QueueController.inst.updateLatestQueue(currentQueue); + await QueueController.inst.updateLatestQueue(currentQueue.value); } @override @@ -262,12 +254,12 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { if (currentQueue.isEmpty) return; await items._execute( selectable: (finalItems) async { - if (currentQueue.firstOrNull is! Selectable) { + if (currentItem.value is! Selectable) { await clearQueue(); } }, youtubeID: (finalItem) async { - if (currentQueue.firstOrNull is! YoutubeID) { + if (currentItem.value is! YoutubeID) { await clearQueue(); } }, @@ -296,7 +288,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { currentCachedVideo.value = null; currentCachedAudio.value = null; _isCurrentAudioFromCache = false; - _isFetchingInfo.value = false; + isFetchingInfo.value = false; _nextSeekSetAudioCache = null; await super.clearQueue(); } @@ -308,10 +300,6 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { NamidaNavigator.inst.popAllMenus(); ScrollSearchController.inst.unfocusKeyboard(); - if (MiniPlayerController.inst.isReorderingQueue) { - MiniPlayerController.inst.reorderingQueueCompleterPlayer ??= Completer(); - } - /// -- Adding videos that may have been cached to VideoController cache map, /// for the sake of playing videos without connection, usually videos are added automatically /// on restart but this keeps things up-to-date. @@ -347,11 +335,10 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { } } - currentItem?._execute( + currentItem.value?._execute( selectable: (finalItems) async => fn(), youtubeID: (finalItem) async => fn(), ); - await MiniPlayerController.inst.reorderingQueueCompleter?.future; // wait if reordering await MiniPlayerController.inst.reorderingQueueCompleterPlayer?.future; // wait if updating lists after reordering } @@ -402,7 +389,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { // ================================== NamidaBasicAudioHandler Overriden ==================================== @override - InterruptionAction defaultOnInterruption(InterruptionType type) => settings.player.onInterrupted[type] ?? InterruptionAction.pause; + InterruptionAction defaultOnInterruption(InterruptionType type) => settings.player.onInterrupted.value[type] ?? InterruptionAction.pause; @override FutureOr itemToDurationInSeconds(Q item) async { @@ -471,7 +458,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { ); }, onRapidDetected: () { - if (isPlaying) { + if (isPlaying.value) { _pausedTemporarily = true; pause(); } @@ -487,13 +474,12 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { MiniPlayerController.inst.reorderingQueueCompleterPlayer?.completeIfWasnt(); } - int get playErrorRemainingSecondsToSkip => _playErrorRemainingSecondsToSkip.value; Timer? _playErrorSkipTimer; - final _playErrorRemainingSecondsToSkip = 0.obs; + final playErrorRemainingSecondsToSkip = 0.obs; void cancelPlayErrorSkipTimer() { _playErrorSkipTimer?.cancel(); _playErrorSkipTimer = null; - _playErrorRemainingSecondsToSkip.value = 0; + playErrorRemainingSecondsToSkip.value = 0; } Future onItemPlaySelectable(Q pi, Selectable item, int index, bool Function() startPlaying, Function skipItem) async { @@ -505,7 +491,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { path: tr.path, duration: Duration(seconds: tr.duration), stillPlaying: (path) { - final current = currentItem; + final current = currentItem.value; return current is Selectable && path == current.track.path; }, ); @@ -522,7 +508,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { Future setPls() async { final dur = await setSource( - tr.toAudioSource(currentIndex, currentQueue.length), + tr.toAudioSource(currentIndex.value, currentQueue.length), item: pi, startPlaying: startPlaying, videoOptions: initialVideo == null @@ -538,14 +524,14 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { ); Indexer.inst.updateTrackDuration(tr, dur); - refreshNotification(currentItem); + refreshNotification(currentItem.value); return dur; } Duration? duration; bool checkInterrupted() { - if (item.track != currentTrack.track) { + if (item.track != currentItem.value) { return true; } else { if (duration != null) _currentItemDuration.value = duration; @@ -558,7 +544,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { if (checkInterrupted()) return; } on Exception catch (e) { if (checkInterrupted()) return; - final reallyError = !(duration != null && currentPositionMS > 0); + final reallyError = !(duration != null && currentPositionMS.value > 0); if (reallyError) { printy(e, isError: true); // -- playing music from root folders still require `all_file_access` @@ -567,13 +553,13 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { if (hadPermissionBefore) { pause(); cancelPlayErrorSkipTimer(); - _playErrorRemainingSecondsToSkip.value = 7; + playErrorRemainingSecondsToSkip.value = 7; _playErrorSkipTimer = Timer.periodic( const Duration(seconds: 1), (timer) { - _playErrorRemainingSecondsToSkip.value--; - if (_playErrorRemainingSecondsToSkip.value <= 0) { + playErrorRemainingSecondsToSkip.value--; + if (playErrorRemainingSecondsToSkip.value <= 0) { NamidaNavigator.inst.closeDialog(); if (currentQueue.length > 1) skipItem(); timer.cancel(); @@ -615,7 +601,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { required String videoId, required NamidaVideo? videoItem, }) async { - final wasPlaying = isPlaying; + final wasPlaying = isPlaying.value; setAudioOnlyPlayback(false); currentVideoStream.value = stream; @@ -633,13 +619,13 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { ); } catch (e) { // ==== if the url got outdated. - _isFetchingInfo.value = true; + isFetchingInfo.value = true; final newStreams = await YoutubeController.inst.getAvailableVideoStreamsOnly(videoId); - _isFetchingInfo.value = false; + isFetchingInfo.value = false; final sameStream = newStreams.firstWhereEff((e) => e.resolution == stream.resolution && e.formatSuffix == stream.formatSuffix); final sameStreamUrl = sameStream?.url; - if (currentItem is YoutubeID && videoId != (currentItem as YoutubeID).id) return; + if (currentItem.value is YoutubeID && videoId != (currentItem.value as YoutubeID).id) return; YoutubeController.inst.currentYTQualities.value = newStreams; @@ -661,7 +647,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { required String videoId, }) async { final position = currentPositionMS; - final wasPlaying = isPlaying; + final wasPlaying = isPlaying.value; currentAudioStream.value = stream; @@ -669,7 +655,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { if (cachedAudio != null && useCache) { await setSource( AudioSource.file(cachedAudio.path, tag: mediaItem), - item: currentItem, + item: currentItem.value, startPlaying: () => wasPlaying, keepOldVideoSource: true, cachedAudioPath: cachedAudio.path, @@ -688,7 +674,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { await _onAudioCacheDone(videoId, cacheFile); }, ), - item: currentItem, + item: currentItem.value, startPlaying: () => wasPlaying, keepOldVideoSource: true, ); @@ -699,13 +685,13 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { await setAudioLockCache(); } catch (e) { // ==== if the url got outdated. - _isFetchingInfo.value = true; + isFetchingInfo.value = true; final newStreams = await YoutubeController.inst.getAvailableAudioOnlyStreams(videoId); - _isFetchingInfo.value = false; + isFetchingInfo.value = false; final sameStream = newStreams.firstWhereEff((e) => e.bitrate == stream.bitrate && e.formatSuffix == stream.formatSuffix); final sameStreamUrl = sameStream?.url; - if (currentItem is YoutubeID && videoId != (currentItem as YoutubeID).id) return; + if (currentItem.value is YoutubeID && videoId != (currentItem.value as YoutubeID).id) return; YoutubeController.inst.currentYTAudioStreams.value = newStreams; @@ -713,7 +699,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { await setAudioLockCache(); } } - await seek(position.milliseconds); + await seek(position.value.milliseconds); if (wasPlaying) { await onPlayRaw(); } @@ -725,13 +711,13 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { Future tryGenerateWaveform(YoutubeID? video) async { if (video != null && WaveformController.inst.isDummy && !settings.youtubeStyleMiniplayer.value) { final audioPath = currentCachedAudio.value?.file.path ?? _nextSeekSetAudioCache?.path; - final dur = currentItemDuration; + final dur = currentItemDuration.value; if (audioPath != null && dur != null) { return WaveformController.inst.generateWaveform( path: audioPath, duration: dur, stillPlaying: (path) => - currentItem is YoutubeID && currentItem == video && (_nextSeekSetAudioCache != null && path == _nextSeekSetAudioCache?.path) || + currentItem.value is YoutubeID && currentItem.value == video && (_nextSeekSetAudioCache != null && path == _nextSeekSetAudioCache?.path) || (currentCachedAudio.value != null && path == currentCachedAudio.value?.file.path), ); } @@ -751,12 +737,12 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { if (audioCacheFile != null) { // -- generating waveform if needed if (WaveformController.inst.isDummy && !settings.youtubeStyleMiniplayer.value) { - final dur = currentItemDuration; + final dur = currentItemDuration.value; if (dur != null) { WaveformController.inst.generateWaveform( path: audioCacheFile.path, duration: dur, - stillPlaying: (path) => currentItem is YoutubeID && _nextSeekSetAudioCache != null && path == _nextSeekSetAudioCache?.path, + stillPlaying: (path) => currentItem.value is YoutubeID && _nextSeekSetAudioCache != null && path == _nextSeekSetAudioCache?.path, ); } } @@ -792,7 +778,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { Function skipItem, { bool? canPlayAudioOnlyFromCache, }) async { - canPlayAudioOnlyFromCache ??= (isAudioOnlyPlayback || !ConnectivityController.inst.hasConnection); + canPlayAudioOnlyFromCache ??= (_isAudioOnlyPlayback || !ConnectivityController.inst.hasConnection); WaveformController.inst.resetWaveform(); @@ -809,7 +795,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { currentCachedVideo.value = null; currentCachedAudio.value = null; _isCurrentAudioFromCache = false; - _isFetchingInfo.value = false; + isFetchingInfo.value = false; _nextSeekSetAudioCache = null; if (item.id == '' || item.id == 'null') { @@ -825,7 +811,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { await onPlayRaw(); } if (sourceChanged) { - await seek(currentPositionMS.milliseconds); + await seek(currentPositionMS.value.milliseconds); } if (!wasPlayingFromCache) { startSleepAfterMinCount(); @@ -838,13 +824,13 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { if (enableCrossFade) { playerStoppingSeikoo.complete(true); } else { - if (isPlaying) { + if (isPlaying.value) { // wait for pausing only if playing. - pauseAndDispose(fadeMS: 100, stillSameItem: () => item == currentVideo).then((_) { + pauseAndDispose(fadeMS: 100, stillSameItem: () => item == currentItem.value).then((_) { playerStoppingSeikoo.complete(true); }); } else { - if (item == currentVideo) await super.onDispose(); + if (item == currentItem.value) await super.onDispose(); playerStoppingSeikoo.complete(true); } } @@ -864,7 +850,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { WaveformController.inst.generateWaveform( path: audioDetails.file.path, duration: dur, - stillPlaying: (path) => currentItem is YoutubeID && path == currentCachedAudio.value?.file.path, + stillPlaying: (path) => currentItem.value is YoutubeID && path == currentCachedAudio.value?.file.path, ); } } @@ -876,7 +862,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { item: item, index: index, canPlayAudioOnly: canPlayAudioOnlyFromCache, - disableVideo: isAudioOnlyPlayback, + disableVideo: _isAudioOnlyPlayback, whatToAwait: () async => await playerStoppingSeikoo.future, startPlaying: startPlaying, possibleAudioFiles: audioCacheMap[item.id] ?? [], @@ -887,7 +873,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { // race avoidance when playing multiple videos bool checkInterrupted() { - if (item != currentVideo) { + if (item != currentItem.value) { return true; } else { if (duration != null) _currentItemDuration.value = duration; @@ -920,30 +906,47 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { if (ConnectivityController.inst.hasConnection) { try { - VideoInfo? info; - var videoStreams = []; + final infoC = Completer(); + final videoStreamsC = Completer>(); + final audiostreamsC = Completer>(); var audiostreams = []; - _isFetchingInfo.value = true; - try { - info = await YoutubeController.inst.fetchVideoDetails(item.id); - audiostreams = await YoutubeController.inst.getAvailableAudioOnlyStreams(item.id); - if (isAudioOnlyPlayback) { - // -- await video streams only if not audio playback - YoutubeController.inst.getAvailableVideoStreamsOnly(item.id).then((value) => videoStreams = value); - } else { - videoStreams = await YoutubeController.inst.getAvailableVideoStreamsOnly(item.id); - } - } catch (e) { - snackyy(message: 'Error getting streams', top: false, isError: true); + var videoStreams = []; + VideoInfo? info; + + isFetchingInfo.value = true; + + YoutubeController.inst.getAvailableAudioOnlyStreams(item.id).catchError((_) { + snackyy(message: 'Error getting audio streams', top: false, isError: true); + return []; + }).then(audiostreamsC.complete); + YoutubeController.inst.fetchVideoDetails(item.id).catchError((_) => null).then(infoC.complete); + YoutubeController.inst.getAvailableVideoStreamsOnly(item.id).catchError((_) => []).then(videoStreamsC.complete); + + void onVideoStreamsObtained(List value) { + videoStreams = value; + if (!checkInterrupted()) YoutubeController.inst.currentYTQualities.value = value; } - _isFetchingInfo.value = false; - if (info == null && audiostreams.isEmpty && videoStreams.isEmpty) return; + + void onAudioStreamsObtained(List value) { + audiostreams = value; + if (!checkInterrupted()) YoutubeController.inst.currentYTAudioStreams.value = value; + } + + void onInfoObtained(VideoInfo? value) { + info = value; + if (!checkInterrupted()) currentVideoInfo.value = value; + } + + // -- await video streams only if not audio playback + _isAudioOnlyPlayback ? videoStreamsC.future.then(onVideoStreamsObtained) : await videoStreamsC.future.then(onVideoStreamsObtained); + await audiostreamsC.future.then(onAudioStreamsObtained); + await infoC.future.then(onInfoObtained); if (checkInterrupted()) return; - YoutubeController.inst.currentYTQualities.value = videoStreams; - YoutubeController.inst.currentYTAudioStreams.value = audiostreams; - currentVideoInfo.value = info; + isFetchingInfo.value = false; + + if (info == null && audiostreams.isEmpty && videoStreams.isEmpty) return; if (checkInterrupted()) return; - final prefferedVideoStream = isAudioOnlyPlayback || videoStreams.isEmpty ? null : YoutubeController.inst.getPreferredStreamQuality(videoStreams, preferIncludeWebm: false); + final prefferedVideoStream = _isAudioOnlyPlayback || videoStreams.isEmpty ? null : YoutubeController.inst.getPreferredStreamQuality(videoStreams, preferIncludeWebm: false); final prefferedAudioStream = audiostreams.firstWhereEff((e) => e.formatSuffix != 'webm' && e.language == 'en') ?? audiostreams.firstWhereEff((e) => e.formatSuffix != 'webm') ?? audiostreams.firstOrNull; @@ -955,7 +958,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { ? (prefferedVideoStream?.width ?? 0) > (cachedVideoSet.width) : false; - currentVideoStream.value = isAudioOnlyPlayback + currentVideoStream.value = _isAudioOnlyPlayback ? null : isStreamRequiredBetterThanCachedSet ? prefferedVideoStream @@ -974,7 +977,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { if (checkInterrupted()) return; // -- since we disabled auto switching video streams once played from cache, [isVideoCacheSameAsPrevSet] is dropped. // -- with the new possibility of playing local tracks as audio source, [isAudioCacheSameAsPrevSet] also is dropped. - final shouldResetVideoSource = isAudioOnlyPlayback ? false : playedFromCacheDetails.video == null; + final shouldResetVideoSource = _isAudioOnlyPlayback ? false : playedFromCacheDetails.video == null; final shouldResetAudioSource = playedFromCacheDetails.audio == null; // -- updating wether the source has changed, so that play should be triggered again. @@ -1021,7 +1024,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { if (checkInterrupted()) return; void showSnackError(String nextAction) { SchedulerBinding.instance.addPostFrameCallback((timeStamp) { - if (item == currentItem) { + if (item == currentItem.value) { snackyy(message: 'Error playing video, $nextAction: $e', top: false, isError: true); } }); @@ -1034,7 +1037,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { item: item, index: index, canPlayAudioOnly: canPlayAudioOnlyFromCache, - disableVideo: isAudioOnlyPlayback, + disableVideo: _isAudioOnlyPlayback, whatToAwait: () async => await playerStoppingSeikoo.future, startPlaying: startPlaying, possibleAudioFiles: audioCacheMap[item.id] ?? [], @@ -1052,17 +1055,17 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { if (currentVideoInfo.value == null) { YoutubeController.inst.fetchVideoDetails(item.id).then((details) { - if (currentItem == item) { + if (currentItem.value == item) { currentVideoInfo.value = details; - refreshNotification(currentItem, currentVideoInfo.value); + refreshNotification(currentItem.value, currentVideoInfo.value); } }); } if (currentVideoThumbnail.value == null) { ThumbnailManager.inst.getYoutubeThumbnailAndCache(id: item.id).then((thumbFile) { - if (currentItem == item) { + if (currentItem.value == item) { currentVideoThumbnail.value = thumbFile; - refreshNotification(currentItem); + refreshNotification(currentItem.value); } }); } @@ -1232,10 +1235,10 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { FutureOr onNotificationFavouriteButtonPressed(Q item) async { await item._execute( selectable: (finalItem) async { - final newStat = await PlaylistController.inst.favouriteButtonOnPressed(Player.inst.nowPlayingTrack); + final newStat = await PlaylistController.inst.favouriteButtonOnPressed(finalItem.track); _notificationUpdateItem( item: item, - itemIndex: currentIndex, + itemIndex: currentIndex.value, isItemFavourite: newStat, ); }, @@ -1252,7 +1255,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { _resourcesDisposeTimer = null; } else { _resourcesDisposeTimer ??= Timer(const Duration(minutes: 5), () { - if (!this.isPlaying) stop(); + if (!this.isPlaying.value) stop(); }); } } @@ -1268,8 +1271,11 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { // saves the file each 20 seconds. if (newSeconds % 20 == 0) { - _updateTrackLastPosition(currentTrack.track, currentPositionMS); - await File(AppPaths.TOTAL_LISTEN_TIME).writeAsJson(totalTimeInSeconds); + final ci = currentItem.value; + if (ci is Selectable) { + _updateTrackLastPosition(ci.track, currentPositionMS.value); + await File(AppPaths.TOTAL_LISTEN_TIME).writeAsJson(totalTimeInSeconds); + } } } @@ -1277,7 +1283,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { FutureOr onItemLastPositionReport(Q? currentItem, int currentPositionMs) async { await currentItem?._execute( selectable: (finalItem) async { - await _updateTrackLastPosition(finalItem.track, currentPositionMS); + await _updateTrackLastPosition(finalItem.track, currentPositionMS.value); }, youtubeID: (finalItem) async {}, ); @@ -1285,14 +1291,14 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { @override void onPlaybackEventStream(PlaybackEvent event) { - final item = currentItem; + final item = currentItem.value; item?._execute( selectable: (finalItem) async { final isFav = finalItem.track.isFavourite; - playbackState.add(transformEvent(event, isFav, currentIndex)); + playbackState.add(transformEvent(event, isFav, currentIndex.value)); }, youtubeID: (finalItem) async { - playbackState.add(transformEvent(event, false, currentIndex)); + playbackState.add(transformEvent(event, false, currentIndex.value)); }, ); } @@ -1310,7 +1316,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { @override PlayerConfig get defaultPlayerConfig => PlayerConfig( - skipSilence: settings.player.skipSilenceEnabled.value && currentVideo == null, + skipSilence: settings.player.skipSilenceEnabled.value && currentItem.value is! YoutubeID, speed: settings.player.speed.value, volume: _userPlayerVolume, pitch: settings.player.pitch.value, @@ -1319,7 +1325,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { double get _userPlayerVolume => settings.player.volume.value; @override - bool get enableCrossFade => settings.player.enableCrossFade.value && currentQueueYoutubeID.isEmpty; + bool get enableCrossFade => settings.player.enableCrossFade.value && currentItem.value is! YoutubeID; @override int get defaultCrossFadeMilliseconds => settings.player.crossFadeDurationMS.value; @@ -1383,7 +1389,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { // ------------------------------------------------------------ Future togglePlayPause() async { - if (isPlaying) { + if (isPlaying.value) { await pause(); } else { await play(); @@ -1394,12 +1400,12 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { Future seek(Duration position) async { Future plsSeek() async => await super.seek(position); - await currentItem?._execute( + await currentItem.value?._execute( selectable: (finalItem) async { await plsSeek(); }, youtubeID: (finalItem) async { - final wasPlaying = isPlaying; + final wasPlaying = isPlaying.value; final cachedAudioFile = _nextSeekSetAudioCache; if (cachedAudioFile != null) { await onPauseRaw(); @@ -1408,7 +1414,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { if (await cachedAudioFile.exists()) { await setSource( AudioSource.file(cachedAudioFile.path, tag: mediaItem), - item: currentItem, + item: currentItem.value, keepOldVideoSource: true, cachedAudioPath: cachedAudioFile.path, startPlaying: () => wasPlaying, @@ -1429,13 +1435,13 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { if (previousButtonReplays) { final int secondsToReplay; if (settings.player.isSeekDurationPercentage.value) { - final sFromP = (currentItemDuration?.inSeconds ?? 0) * (settings.player.seekDurationInPercentage.value / 100); + final sFromP = (currentItemDuration.value?.inSeconds ?? 0) * (settings.player.seekDurationInPercentage.value / 100); secondsToReplay = sFromP.toInt(); } else { secondsToReplay = settings.player.seekDurationInSeconds.value; } - if (secondsToReplay > 0 && currentPositionMS > secondsToReplay * 1000) { + if (secondsToReplay > 0 && currentPositionMS.value > secondsToReplay * 1000) { await seek(Duration.zero); return; } diff --git a/lib/base/generator_base.dart b/lib/base/generator_base.dart index 5060016e..b4f6fb75 100644 --- a/lib/base/generator_base.dart +++ b/lib/base/generator_base.dart @@ -53,7 +53,7 @@ abstract class NamidaGeneratorBase { final subItem = itemToSub(t); if (subItem == item) { final heatTracks = historytracks.getRange(clamped(i - length), clamped(i + length)).toList(); - heatTracks.loop((e, index) { + heatTracks.loop((e) { numberOfListensMap.update(itemToSub(e), (value) => value + 1, ifAbsent: () => 1); }); // skip length since we already took 10 tracks. diff --git a/lib/base/history_days_rebuilder.dart b/lib/base/history_days_rebuilder.dart new file mode 100644 index 00000000..8a0620d6 --- /dev/null +++ b/lib/base/history_days_rebuilder.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:history_manager/history_manager.dart'; + +mixin HistoryDaysRebuilderMixin on State { + HistoryManager get historyManager; + + Iterable getHistoryTracks(Map> history) sync* { + final map = history; + for (final trs in map.values) { + yield* trs; + } + } + + int dayToMillis(int day) => day * 24 * 60 * 60 * 1000; + + List _listifyHistoryDays() => historyManager.historyMap.value.keys.toList(); + + late List historyDays = _listifyHistoryDays(); + + void _updateDays() { + if (mounted) { + historyDays = _listifyHistoryDays(); + } + } + + @override + void initState() { + historyManager.modifiedDays.addListener(_updateDays); + super.initState(); + } + + @override + void dispose() { + historyManager.modifiedDays.removeListener(_updateDays); + super.dispose(); + } +} diff --git a/lib/base/pull_to_refresh.dart b/lib/base/pull_to_refresh.dart index 42eb25e2..ede1841a 100644 --- a/lib/base/pull_to_refresh.dart +++ b/lib/base/pull_to_refresh.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; +import 'package:namida/core/utils.dart'; mixin PullToRefreshMixin on State implements TickerProvider { + bool enablePullToRefresh = true; + final turnsTween = Tween(begin: 0.0, end: 1.0); late final animation = AnimationController(vsync: this, duration: Duration.zero); @@ -27,11 +29,14 @@ mixin PullToRefreshMixin on State implements Ticker } void onPointerMove(ScrollController sc, PointerMoveEvent event) { + if (!enablePullToRefresh) return; final dy = event.delta.dy; if (_isDraggingVertically == null) { - final canDragVertically = dy < 0 || (sc.hasClients && sc.position.pixels <= 0); - final horizontalAllowance = event.delta.dx.abs() < 0.1; - _isDraggingVertically = canDragVertically && horizontalAllowance; + try { + final canDragVertically = dy < 0 || (sc.hasClients && sc.positions.first.pixels <= 0); + final horizontalAllowance = event.delta.dx.abs() < 0.1; + _isDraggingVertically = canDragVertically && horizontalAllowance; + } catch (_) {} } if (_isDraggingVertically == true) _onVerticalDragUpdate(dy); } @@ -44,6 +49,7 @@ mixin PullToRefreshMixin on State implements Ticker bool _isRefreshing = false; Future onRefresh(Future Function() execute) async { + if (!enablePullToRefresh) return; onVerticalDragFinish(); if (animation.value != 1 || _isRefreshing) return; _isRefreshing = true; diff --git a/lib/base/youtube_channel_controller.dart b/lib/base/youtube_channel_controller.dart index cbd3b1cf..533f29ea 100644 --- a/lib/base/youtube_channel_controller.dart +++ b/lib/base/youtube_channel_controller.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; +import 'package:namida/base/youtube_streams_manager.dart'; import 'package:namida/controller/connectivity.dart'; import 'package:namida/controller/current_color.dart'; import 'package:namida/core/extensions.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/youtube/class/youtube_subscription.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; -import 'package:namida/base/youtube_streams_manager.dart'; import 'package:namida/youtube/controller/youtube_subscriptions_controller.dart'; abstract class YoutubeChannelController extends State with YoutubeStreamsManager { @@ -44,7 +44,7 @@ abstract class YoutubeChannelController extends State< void updatePeakDates(List streams) { int oldest = (streamsPeakDates?.oldest ?? DateTime.now()).millisecondsSinceEpoch; int newest = (streamsPeakDates?.newest ?? DateTime(0)).millisecondsSinceEpoch; - streams.loop((e, _) { + streams.loop((e) { final d = e.date; if (d != null) { final ms = d.millisecondsSinceEpoch; diff --git a/lib/base/youtube_streams_manager.dart b/lib/base/youtube_streams_manager.dart index 1bdf5ee7..23acc71b 100644 --- a/lib/base/youtube_streams_manager.dart +++ b/lib/base/youtube_streams_manager.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; enum YTVideosSorting { @@ -51,15 +51,16 @@ mixin YoutubeStreamsManager { mainAxisSize: MainAxisSize.min, children: [ enabled - ? Obx( - () => StackedIcon( + ? ObxO( + rx: sortingByTop, + builder: (sortingByTop) => StackedIcon( baseIcon: details.$2, - secondaryIcon: sortingByTop.value ? Broken.arrow_down_2 : Broken.arrow_up_3, + secondaryIcon: sortingByTop ? Broken.arrow_down_2 : Broken.arrow_up_3, iconSize: 20.0, secondaryIconSize: 10.0, blurRadius: 4.0, baseIconColor: itemsColor, - // secondaryIconColor: enabled ? context.theme.colorScheme.background : null, + // secondaryIconColor: enabled ? context.theme.colorScheme.surface : null, ), ) : Icon( @@ -121,9 +122,7 @@ mixin YoutubeStreamsManager { case YTVideosSorting.date: return (lang.DATE, Broken.calendar); case YTVideosSorting.views: - final word = lang.VIEWS; - final finalw = word.length > 1 ? "${word[0].toUpperCase()}${word.substring(1)}" : word; - return (finalw, Broken.eye); + return (lang.VIEWS.capitalizeFirst(), Broken.eye); case YTVideosSorting.duration: return (lang.DURATION, Broken.timer_1); } diff --git a/lib/class/folder.dart b/lib/class/folder.dart index d86d995f..b9b37d3f 100644 --- a/lib/class/folder.dart +++ b/lib/class/folder.dart @@ -55,7 +55,7 @@ class Folder { int get hashCode => _key.hashCode; @override - String toString() => "Folder(path: $path, tracks: ${tracks.length})"; + String toString() => "Folder(path: $path, tracks: ${tracks().length})"; } extension FolderUtils on Folder { @@ -69,7 +69,7 @@ extension FolderUtils on Folder { /// Can be heplful to display full path in such case. bool get hasSimilarFolderNames { int count = 0; - for (final k in Indexer.inst.mainMapFolders.keys) { + for (final k in Indexer.inst.mainMapFolders.value.keys) { if (k.folderName == folderName) { count++; if (count > 1) return true; @@ -78,9 +78,10 @@ extension FolderUtils on Folder { return false; } - List get tracks => Indexer.inst.mainMapFolders[this] ?? []; - Iterable get tracksRecusive sync* { - for (final e in Indexer.inst.mainMapFolders.entries) { + List tracks() => Indexer.inst.mainMapFolders.value[this] ?? []; + + Iterable tracksRecusive() sync* { + for (final e in Indexer.inst.mainMapFolders.value.entries) { if (this.isParentOf(e.key)) { yield* e.value; } @@ -94,7 +95,7 @@ extension FolderUtils on Folder { while (parts.isNotEmpty) { final f = Folder(parts.join(_pathSeparator)); - if (Indexer.inst.mainMapFolders[f] != null) return f; + if (Indexer.inst.mainMapFolders.value[f] != null) return f; parts.removeLast(); } @@ -108,7 +109,7 @@ extension FolderUtils on Folder { final splitsCount = this.splitParts().length; - for (final folder in foldersMap.keys) { + for (final folder in foldersMap.value.keys) { if (this.isDirectParentOf(folder, splitsCount)) allInside.add(folder); } diff --git a/lib/class/library_item_map.dart b/lib/class/library_item_map.dart index d0fedd37..276128fc 100644 --- a/lib/class/library_item_map.dart +++ b/lib/class/library_item_map.dart @@ -1,12 +1,17 @@ +// ignore_for_file: avoid_rx_value_getter_outside_obx import 'dart:collection'; -import 'package:get/get_rx/src/rx_types/rx_types.dart'; - import 'package:namida/class/track.dart'; +import 'package:namida/core/utils.dart'; class LibraryItemMap { LibraryItemMap() : _value = LinkedHashMap>(equals: (item1, item2) => item1.toLowerCase() == item2.toLowerCase()).obs; final Rx>> _value; LinkedHashMap> get value => _value.value; + LinkedHashMap> get valueR => _value.valueR; void refresh() => _value.refresh(); + void clear() { + _value.value.clear(); + refresh(); + } } diff --git a/lib/class/queue.dart b/lib/class/queue.dart index bc983a17..c862ce9e 100644 --- a/lib/class/queue.dart +++ b/lib/class/queue.dart @@ -1,8 +1,6 @@ import 'package:namida/class/track.dart'; -import 'package:namida/core/constants.dart'; import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; -import 'package:namida/core/functions.dart'; class Queue { final QueueSource source; @@ -19,15 +17,10 @@ class Queue { required this.tracks, }); - /// Converts empty queue to AllTracksList. + /// // Converts empty queue to AllTracksList. + /// BREAKING(>v2.5.6): no longer reads empty queue as allTracks. factory Queue.fromJson(Map json) { - final res = (json['tracks'] as List? ?? []).mapped((e) => (e as String).toTrack()); - final finalTracks = []; - if (res.isEmpty) { - finalTracks.addAll(allTracksInLibrary); - } else { - finalTracks.addAll(res); - } + final finalTracks = (json['tracks'] as List? ?? []).mapped((e) => (e as String).toTrack()); return Queue( source: QueueSource.values.getEnum(json['source'] ?? '') ?? QueueSource.others, homePageItem: HomePageItems.values.getEnum(json['homePageItem'] ?? ''), @@ -37,16 +30,16 @@ class Queue { ); } - /// Saves an empty queue in case its the same as the AllTracksList. - /// this should lower startup time and increase performance. + /// // Saves an empty queue in case its the same as the AllTracksList. + /// // this should lower startup time and increase performance. + /// BREAKING(>v2.5.6): no longer saving allTracks as empty queue. Map toJson() { - final finalTracks = checkIfQueueSameAsAllTracks(tracks) ? [] : tracks; return { 'source': source.convertToString, 'homePageItem': homePageItem?.convertToString, 'date': date, 'isFav': isFav, - 'tracks': finalTracks.mapped((e) => e.path), + 'tracks': tracks.mapped((e) => e.path), }; } diff --git a/lib/class/split_config.dart b/lib/class/split_config.dart index c47447a4..c6222aa8 100644 --- a/lib/class/split_config.dart +++ b/lib/class/split_config.dart @@ -16,8 +16,8 @@ class ArtistsSplitConfig extends _SplitterConfig { }) { return ArtistsSplitConfig( addFeatArtist: addFeatArtist ?? settings.extractFeatArtistFromTitle.value, - separators: separators ?? settings.trackArtistsSeparators.toList(), - separatorsBlacklist: separatorsBlacklist ?? settings.trackArtistsSeparatorsBlacklist.toList(), + separators: separators ?? settings.trackArtistsSeparators.value, + separatorsBlacklist: separatorsBlacklist ?? settings.trackArtistsSeparatorsBlacklist.value, ); } @@ -49,8 +49,8 @@ class GenresSplitConfig extends _SplitterConfig { final List? separatorsBlacklist, }) { return GenresSplitConfig( - separators: separators ?? settings.trackGenresSeparators.toList(), - separatorsBlacklist: separatorsBlacklist ?? settings.trackGenresSeparatorsBlacklist.toList(), + separators: separators ?? settings.trackGenresSeparators.value, + separatorsBlacklist: separatorsBlacklist ?? settings.trackGenresSeparatorsBlacklist.value, ); } diff --git a/lib/class/track.dart b/lib/class/track.dart index 1c473a70..e1984415 100644 --- a/lib/class/track.dart +++ b/lib/class/track.dart @@ -1,3 +1,4 @@ +// ignore_for_file: avoid_rx_value_getter_outside_obx import 'dart:io'; import 'package:history_manager/history_manager.dart'; @@ -339,7 +340,7 @@ extension TrackExtUtils on TrackExtended { return "${AppDirs.ARTWORKS}$identifier.png"; } - String get albumIdentifier => getAlbumIdentifier(settings.albumIdentifiers); + String get albumIdentifier => getAlbumIdentifier(settings.albumIdentifiers.value); String getAlbumIdentifier(List identifiers) { final n = identifiers.contains(AlbumIdentifier.albumName) ? album : ''; @@ -358,7 +359,7 @@ extension TrackExtUtils on TrackExtended { String get youtubeID => youtubeLink.getYoutubeID; - TrackStats get stats => Indexer.inst.trackStatsMap[toTrack()] ?? TrackStats(kDummyTrack, 0, [], [], 0); + TrackStats get stats => Indexer.inst.trackStatsMap.value[toTrack()] ?? TrackStats(kDummyTrack, 0, [], [], 0); String get yearPreferyyyyMMdd { final tostr = year.toString(); @@ -469,9 +470,10 @@ extension TrackUtils on Track { TrackExtended? toTrackExtOrNull() => path.toTrackExtOrNull(); set duration(int value) { - final trx = Indexer.inst.allTracksMappedByPath[this]; + final trx = Indexer.inst.allTracksMappedByPath.value[this]; if (trx != null) { - Indexer.inst.allTracksMappedByPath[this] = trx.copyWith(duration: value); + Indexer.inst.allTracksMappedByPath.value[this] = trx.copyWith(duration: value); + Indexer.inst.allTracksMappedByPath.refresh(); } } diff --git a/lib/controller/backup_controller.dart b/lib/controller/backup_controller.dart index 1de26126..0b2ddad4 100644 --- a/lib/controller/backup_controller.dart +++ b/lib/controller/backup_controller.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:flutter_archive/flutter_archive.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:intl/intl.dart'; import 'package:namida/controller/file_browser.dart'; @@ -25,8 +25,8 @@ class BackupController { static final BackupController _instance = BackupController._internal(); BackupController._internal(); - final RxBool isCreatingBackup = false.obs; - final RxBool isRestoringBackup = false.obs; + final isCreatingBackup = false.obso; + final isRestoringBackup = false.obso; String get _backupDirectoryPath => settings.defaultBackupLocation.value; int get _defaultAutoBackupInterval => settings.autoBackupIntervalDays.value; @@ -35,7 +35,7 @@ class BackupController { final interval = _defaultAutoBackupInterval; if (interval <= 0) return; - if (!await requestManageStoragePermission()) return; + if (!await requestManageStoragePermission(request: false)) return; final sortedBackupFiles = await _getBackupFilesSorted.thready(_backupDirectoryPath); final latestBackup = sortedBackupFiles.firstOrNull; @@ -96,14 +96,14 @@ class BackupController { File? tempAllLocal; File? tempAllYoutube; - await backupItemsPaths.loopFuture((f, index) async { + for (final f in backupItemsPaths) { if (await FileSystemEntity.type(f) == FileSystemEntityType.file) { f.startsWith(AppDirs.YOUTUBE_MAIN_DIRECTORY) ? youtubeFilesOnly.add(File(f)) : localFilesOnly.add(File(f)); } if (await FileSystemEntity.type(f) == FileSystemEntityType.directory) { dirsOnly.add(Directory(f)); } - }); + } try { for (final d in dirsOnly) { @@ -155,7 +155,7 @@ class BackupController { final possibleFiles = dir.listSyncSafe(); final List matchingBackups = []; - possibleFiles.loop((pf, index) { + possibleFiles.loop((pf) { if (pf is File) { if (pf.path.getFilename.startsWith('Namida Backup - ')) { matchingBackups.add(pf); @@ -174,7 +174,7 @@ class BackupController { final possibleFiles = dir.listSyncSafe(); final statsLookup = {}; - possibleFiles.loop((pf, index) { + possibleFiles.loop((pf) { if (pf is File) { final filename = pf.path.getFilename; if (filename.startsWith('Namida Backup - ') && filename.endsWith(" - auto.zip")) { diff --git a/lib/controller/clipboard_controller.dart b/lib/controller/clipboard_controller.dart index bfd0926e..f6e22474 100644 --- a/lib/controller/clipboard_controller.dart +++ b/lib/controller/clipboard_controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; class ClipboardController { static ClipboardController get inst => _instance; @@ -39,12 +39,12 @@ class ClipboardController { _textInControllerEmpty.value = empty; } - bool get textInControllerEmpty => _textInControllerEmpty.value; + RxBaseCore get textInControllerEmpty => _textInControllerEmpty; final _textInControllerEmpty = true.obs; - String get lastCopyUsed => _lastCopyUsed.value; + RxBaseCore get lastCopyUsed => _lastCopyUsed; final _lastCopyUsed = ''.obs; - String get clipboardText => _clipboardText.value; + RxBaseCore get clipboardText => _clipboardText; final _clipboardText = ''.obs; } diff --git a/lib/controller/connectivity.dart b/lib/controller/connectivity.dart index c865114a..0ebb2c5a 100644 --- a/lib/controller/connectivity.dart +++ b/lib/controller/connectivity.dart @@ -1,27 +1,40 @@ +// ignore_for_file: avoid_rx_value_getter_outside_obx import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; class ConnectivityController { static ConnectivityController get inst => _instance; static final ConnectivityController _instance = ConnectivityController._internal(); ConnectivityController._internal(); - StreamSubscription? _streamSub; + StreamSubscription>? _streamSub; void initialize() { _streamSub?.cancel(); - _streamSub = _connectivity.onConnectivityChanged.listen((connection) { - _connectionType.value = connection; - _hasConnection.value = connection != ConnectivityResult.none; + _streamSub = _connectivity.onConnectivityChanged.listen((connections) { + if (connections.contains(ConnectivityResult.none)) { + _hasConnection.value = false; + _hasHighConnection.value = false; + } else { + final highConnection = connections.contains(ConnectivityResult.wifi) || + connections.contains(ConnectivityResult.ethernet) || + connections.contains(ConnectivityResult.vpn) || + connections.contains(ConnectivityResult.other); + _hasHighConnection.value = highConnection; + _hasConnection.value = true; + } }); } bool get hasConnection => _hasConnection.value; - ConnectivityResult get connectionType => _connectionType.value; + bool get hasHighConnection => _hasHighConnection.value; + + bool get hasConnectionR => _hasConnection.valueR; + bool get hasHighConnectionR => _hasHighConnection.valueR; final _connectivity = Connectivity(); final _hasConnection = false.obs; - final _connectionType = ConnectivityResult.none.obs; + final _hasHighConnection = false.obs; } diff --git a/lib/controller/current_color.dart b/lib/controller/current_color.dart index 0a719fed..c29325fd 100644 --- a/lib/controller/current_color.dart +++ b/lib/controller/current_color.dart @@ -1,3 +1,4 @@ +// ignore_for_file: avoid_rx_value_getter_outside_obx import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; @@ -6,7 +7,6 @@ import 'dart:ui' as ui; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:get/get.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:queue/queue.dart' as qs; @@ -19,11 +19,12 @@ 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/core/utils.dart'; import 'package:namida/youtube/class/youtube_id.dart'; -Color get playerStaticColor => Get.isDarkMode ? playerStaticColorDark : playerStaticColorLight; -Color get playerStaticColorLight => Color(settings.staticColor.value); -Color get playerStaticColorDark => Color(settings.staticColorDark.value); +Color get playerStaticColor => namida.isDarkMode ? playerStaticColorDark : playerStaticColorLight; +Color get playerStaticColorLight => Color(settings.staticColor.valueR); +Color get playerStaticColorDark => Color(settings.staticColorDark.valueR); class CurrentColor { static CurrentColor get inst => _instance; @@ -31,12 +32,13 @@ class CurrentColor { CurrentColor._internal(); bool get _canAutoUpdateColor => settings.autoColor.value || settings.forceMiniplayerTrackColor.value; - Color get miniplayerColor => settings.forceMiniplayerTrackColor.value ? _namidaColorMiniplayer.value ?? color : color; - Color get color => _namidaColor.value.color; - List get palette => _namidaColor.value.palette; - Color get currentColorScheme => _colorSchemeOfSubPages.value ?? color; - int get colorAlpha => Get.isDarkMode ? 200 : 120; - bool get shouldUpdateFromDeviceWallpaper => settings.pickColorsFromDeviceWallpaper.value; + bool get _shouldUpdateFromDeviceWallpaper => settings.pickColorsFromDeviceWallpaper.value; + + Color get miniplayerColor => settings.forceMiniplayerTrackColor.valueR ? _namidaColorMiniplayer.valueR ?? color : color; + Color get color => _namidaColor.valueR.color; + List get palette => _namidaColor.valueR.palette; + Color get currentColorScheme => _colorSchemeOfSubPages.valueR ?? color; + int get colorAlpha => namida.isDarkMode ? 200 : 120; final _namidaColorMiniplayer = Rxn(); @@ -59,6 +61,11 @@ class CurrentColor { final isGeneratingAllColorPalettes = false.obs; + void refreshhh() { + _namidaColor.refresh(); + _namidaColorMiniplayer.refresh(); + } + final _colorsMap = {}; final _colorsMapYTID = {}; @@ -66,18 +73,18 @@ class CurrentColor { void switchColorPalettes(bool isPlaying) { _colorsSwitchTimer?.cancel(); _colorsSwitchTimer = null; - if (Player.inst.currentQueue.isEmpty && Player.inst.currentQueueYoutube.isEmpty) return; + if (Player.inst.currentItem.value == null) return; final durms = isPlaying ? 150 : 2200; _colorsSwitchTimer = Timer.periodic(Duration(milliseconds: durms), (timer) { if (settings.enablePartyModeColorSwap.value) { if (paletteFirstHalf.isEmpty) return; - final lastItem1 = paletteFirstHalf.last; + final lastItem1 = paletteFirstHalf.value.last; paletteFirstHalf.remove(lastItem1); paletteFirstHalf.insertSafe(0, lastItem1); if (paletteSecondHalf.isEmpty) return; - final lastItem2 = paletteSecondHalf.last; + final lastItem2 = paletteSecondHalf.value.last; paletteSecondHalf.remove(lastItem2); paletteSecondHalf.insertSafe(0, lastItem2); } @@ -120,7 +127,7 @@ class CurrentColor { Future refreshColorsAfterResumeApp() async { final namidaColor = await getPlayerColorFromDeviceWallpaper(forceCheck: true); - if (namidaColor != null && settings.autoColor.value && shouldUpdateFromDeviceWallpaper) { + if (namidaColor != null && settings.autoColor.value && _shouldUpdateFromDeviceWallpaper) { _namidaColor.value = namidaColor; _updateCurrentPaletteHalfs(namidaColor); } @@ -155,7 +162,7 @@ class CurrentColor { if (!updateIndexOnly && track != null) { _updatePlayerColorFromItem( getColorPalette: () async => await getTrackColors(track.track), - stillPlaying: () => track.track == Player.inst.nowPlayingTrack, + stillPlaying: () => track.track == Player.inst.currentTrack?.track, ); } if (track != null) { @@ -172,7 +179,7 @@ class CurrentColor { 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; + bool stillPlaying() => ytIdItem.id == Player.inst.currentVideo?.id; _updatePlayerColorFromItem( getColorPalette: () async { @@ -206,7 +213,7 @@ class CurrentColor { _namidaColorMiniplayer.value = trColors.color; if (settings.autoColor.value) { - if (shouldUpdateFromDeviceWallpaper) { + if (_shouldUpdateFromDeviceWallpaper) { namidaColor = await getPlayerColorFromDeviceWallpaper(); } else { namidaColor = trColors; @@ -365,8 +372,8 @@ class CurrentColor { final nc = await extractPaletteFromImage(imagePath, track: track, forceReExtract: true, useIsolate: useIsolate); _updateInColorMap(filename, nc); } - if (Player.inst.nowPlayingTWD == track) { - updatePlayerColorFromTrack(Player.inst.nowPlayingTWD, null); + if (Player.inst.currentTrack == track) { + updatePlayerColorFromTrack(Player.inst.currentTrack, null); } } @@ -444,13 +451,21 @@ class CurrentColor { paletteFirstHalf.clear(); paletteSecondHalf.clear(); - nc.palette.loop((c, i) { - if (i <= halfIndex) { - paletteFirstHalf.add(c); - } else { - paletteSecondHalf.add(c); - } - }); + paletteFirstHalf.execute( + (firstPart) { + paletteSecondHalf.execute( + (secondPart) { + nc.palette.loopAdv((c, i) { + if (i <= halfIndex) { + firstPart.add(c); + } else { + secondPart.add(c); + } + }); + }, + ); + }, + ); } Future generateAllColorPalettes() async { diff --git a/lib/controller/edit_delete_controller.dart b/lib/controller/edit_delete_controller.dart index 87d9f19c..5905a946 100644 --- a/lib/controller/edit_delete_controller.dart +++ b/lib/controller/edit_delete_controller.dart @@ -23,7 +23,7 @@ class EditDeleteController { Future deleteCachedVideos(List tracks) async { final videosToDelete = []; - tracks.loop((e, index) { + tracks.loop((e) { videosToDelete.addAll(VideoController.inst.getNVFromID(e.track.youtubeID)); }); await Indexer.inst.clearVideoCache(videosToDelete); @@ -57,7 +57,7 @@ class EditDeleteController { /// returns failed deletes. static int _deleteAllIsolate(List files) { int failed = 0; - files.loop((e, index) { + files.loop((e) { try { File(e).deleteSync(); } catch (_) { @@ -71,7 +71,7 @@ class EditDeleteController { static ({int deletedCount, int sizeOfDeleted}) _deleteAllWithDetailsIsolate(List files) { int deleted = 0; int size = 0; - files.loop((e, index) { + files.loop((e) { int s = 0; try { s = File(e).lengthSync(); @@ -132,7 +132,7 @@ class EditDeleteController { final oldNewTrack = oldNewMap; for (final oldNewTrack in oldNewTrack.entries) { - allHistory.loop((entry, index) { + allHistory.loop((entry) { final day = entry.key; final trs = entry.value; trs.replaceWhere( @@ -146,13 +146,14 @@ class EditDeleteController { ); }); } + HistoryController.inst.historyMap.refresh(); await Future.wait([ HistoryController.inst.saveHistoryToStorage(daysToSave).then((value) => HistoryController.inst.updateMostPlayedPlaylist()), QueueController.inst.replaceTrackInAllQueues(oldNewTrack), // -- Queues PlaylistController.inst.replaceTrackInAllPlaylistsBulk(oldNewTrack), // -- Playlists ]); // -- Selected Tracks - if (SelectedTracksController.inst.selectedTracks.isNotEmpty) { + if (SelectedTracksController.inst.selectedTracks.value.isNotEmpty) { for (final oldNewTrack in oldNewTrack.entries) { SelectedTracksController.inst.replaceThisTrack(oldNewTrack.key, oldNewTrack.value); } @@ -170,7 +171,7 @@ class EditDeleteController { HistoryController.inst.replaceAllTracksInsideHistory(oldTrack, newTrack), // History ]); // --- Selected Tracks --- - if (SelectedTracksController.inst.selectedTracks.isNotEmpty) { + if (SelectedTracksController.inst.selectedTracks.value.isNotEmpty) { SelectedTracksController.inst.replaceThisTrack(oldTrack, newTrack); } } @@ -186,7 +187,7 @@ class EditDeleteController { Player.inst.replaceTracksDirectoryInQueue(oldDir, newDir, forThesePathsOnly: forThesePathsOnly, ensureNewFileExists: ensureNewFileExists), HistoryController.inst.replaceTracksDirectoryInHistory(oldDir, newDir, forThesePathsOnly: forThesePathsOnly, ensureNewFileExists: ensureNewFileExists), ]); - if (SelectedTracksController.inst.selectedTracks.isNotEmpty) { + if (SelectedTracksController.inst.selectedTracks.value.isNotEmpty) { SelectedTracksController.inst.replaceTrackDirectory(oldDir, newDir, forThesePathsOnly: forThesePathsOnly, ensureNewFileExists: ensureNewFileExists); } } diff --git a/lib/controller/ffmpeg_controller.dart b/lib/controller/ffmpeg_controller.dart index 8df91915..19a44ae8 100644 --- a/lib/controller/ffmpeg_controller.dart +++ b/lib/controller/ffmpeg_controller.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:ffmpeg_kit_flutter_min/ffmpeg_kit.dart'; import 'package:ffmpeg_kit_flutter_min/ffmpeg_kit_config.dart'; import 'package:ffmpeg_kit_flutter_min/ffprobe_kit.dart'; -import 'package:get/get_rx/get_rx.dart'; import 'package:namida/class/media_info.dart'; import 'package:namida/class/track.dart'; @@ -13,6 +12,7 @@ import 'package:namida/controller/tagger_controller.dart'; import 'package:namida/controller/thumbnail_manager.dart'; import 'package:namida/core/constants.dart'; import 'package:namida/core/extensions.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/main.dart'; class NamidaFFMPEG { @@ -255,7 +255,7 @@ class NamidaFFMPEG { final allFiles = []; int remainingDirsLength = directoriesPaths.length; final completer = Completer(); - directoriesPaths.loop((e, index) { + directoriesPaths.loop((e) { Directory(e).listAllIsolate(recursive: recursive).then( (value) { allFiles.addAll(value); diff --git a/lib/controller/file_browser.dart b/lib/controller/file_browser.dart index 66e37803..c74a75da 100644 --- a/lib/controller/file_browser.dart +++ b/lib/controller/file_browser.dart @@ -4,7 +4,7 @@ import 'dart:isolate'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/base/pull_to_refresh.dart'; import 'package:namida/controller/namida_channel_storage.dart'; @@ -42,6 +42,7 @@ class NamidaFileBrowser { memeType: memeType, initialDirectory: initialDirectory, onNavigate: _onNavigate, + onPop: _onPop, ); } @@ -57,6 +58,7 @@ class NamidaFileBrowser { memeType: memeType, initialDirectory: initialDirectory, onNavigate: _onNavigate, + onPop: _onPop, ); } @@ -68,6 +70,7 @@ class NamidaFileBrowser { note: note, initialDirectory: initialDirectory, onNavigate: _onNavigate, + onPop: _onPop, ); } @@ -79,6 +82,7 @@ class NamidaFileBrowser { note: note, initialDirectory: initialDirectory, onNavigate: _onNavigate, + onPop: _onPop, ); } @@ -91,20 +95,19 @@ class NamidaFileBrowser { } static void _onNavigate(_NamidaFileBrowserBase widget) { - Get.to( - () => widget, - id: null, - preventDuplicates: false, + NamidaNavigator.inst.navigateToRoot( + widget, transition: Transition.native, - curve: Curves.easeOut, - duration: const Duration(milliseconds: 300), - opaque: true, - fullscreenDialog: false, ); } + + static void _onPop() { + NamidaNavigator.inst.popRoot(); + } } typedef _NamidaFileBrowserNavigationCallback = void Function(_NamidaFileBrowserBase options); +typedef _NamidaFileBrowserPopCallback = void Function(); class _NamidaFileBrowserBase extends StatefulWidget { final String note; @@ -113,6 +116,7 @@ class _NamidaFileBrowserBase extends StatefulWidget final List allowedExtensions; final String memeType; final bool allowMultiple; + final _NamidaFileBrowserPopCallback onPop; const _NamidaFileBrowserBase({ super.key, @@ -122,6 +126,7 @@ class _NamidaFileBrowserBase extends StatefulWidget this.allowedExtensions = const [], this.memeType = _defaultMemeType, required this.allowMultiple, + required this.onPop, }); static Future pickFile({ @@ -130,6 +135,7 @@ class _NamidaFileBrowserBase extends StatefulWidget String memeType = _defaultMemeType, String? initialDirectory, required _NamidaFileBrowserNavigationCallback onNavigate, + required _NamidaFileBrowserPopCallback onPop, }) async { final completer = Completer>(); onNavigate( @@ -140,6 +146,7 @@ class _NamidaFileBrowserBase extends StatefulWidget memeType: memeType, onSelect: completer, allowMultiple: false, + onPop: onPop, ), ); final all = await completer.future; @@ -152,6 +159,7 @@ class _NamidaFileBrowserBase extends StatefulWidget String memeType = _defaultMemeType, String? initialDirectory, required _NamidaFileBrowserNavigationCallback onNavigate, + required _NamidaFileBrowserPopCallback onPop, }) async { final completer = Completer>(); onNavigate( @@ -162,6 +170,7 @@ class _NamidaFileBrowserBase extends StatefulWidget memeType: memeType, onSelect: completer, allowMultiple: true, + onPop: onPop, ), ); return completer.future; @@ -171,6 +180,7 @@ class _NamidaFileBrowserBase extends StatefulWidget String note = '', String? initialDirectory, required _NamidaFileBrowserNavigationCallback onNavigate, + required _NamidaFileBrowserPopCallback onPop, }) async { final completer = Completer>(); onNavigate( @@ -179,6 +189,7 @@ class _NamidaFileBrowserBase extends StatefulWidget initialDirectory: initialDirectory, onSelect: completer, allowMultiple: false, + onPop: onPop, ), ); final all = await completer.future; @@ -190,6 +201,7 @@ class _NamidaFileBrowserBase extends StatefulWidget String note = '', String? initialDirectory, required _NamidaFileBrowserNavigationCallback onNavigate, + required _NamidaFileBrowserPopCallback onPop, }) async { final completer = Completer>(); onNavigate( @@ -198,6 +210,7 @@ class _NamidaFileBrowserBase extends StatefulWidget initialDirectory: initialDirectory, onSelect: completer, allowMultiple: true, + onPop: onPop, ), ); final res = await completer.future; @@ -424,7 +437,7 @@ class _NamidaFileBrowserState extends State<_NamidaF int totalSize = 0; final subDir = []; final items = dir.listSync(recursive: false); - items.loop((e, _) { + items.loop((e) { if (e is File) { totalSize += onFileAdd(dir, e); } else if (e is Directory) { @@ -432,7 +445,7 @@ class _NamidaFileBrowserState extends State<_NamidaF directoryForFolders.addForce(dir.path, e.path); } }); - subDir.loop((sub, _) { + subDir.loop((sub) { totalSize += dirSafeRecursiveListSync( sub, ); @@ -467,7 +480,7 @@ class _NamidaFileBrowserState extends State<_NamidaF for (final fileEntry in directoryForFiles.entries) { int totalSize = 0; - fileEntry.value.loop((e, _) => totalSize += infoFiles[e]?.size ?? 0); + fileEntry.value.loop((e) => totalSize += infoFiles[e]?.size ?? 0); onDirInfo(fileEntry, totalSize); } for (final dirEntry in directoryForFolders.entries) { @@ -518,7 +531,7 @@ class _NamidaFileBrowserState extends State<_NamidaF // // -- files // if (p.$2.isNotEmpty) { // final newMapFiles = {}; - // p.$2.loop((e, index) { + // p.$2.loop((e){ // try { // if (allFilesStats[e.path] == null) { // final stats = e.statSync(); @@ -538,7 +551,7 @@ class _NamidaFileBrowserState extends State<_NamidaF // // -- dirs // if (p.$3.isNotEmpty) { // final newMapDirs = {}; - // p.$3.loop((dir, index) { + // p.$3.loop((dir){ // try { // int totalSize = 0; // if (allDirsStats[dir.path] == null) { @@ -551,7 +564,7 @@ class _NamidaFileBrowserState extends State<_NamidaF // } // int filesCount = 0; // int foldersCount = 0; - // itemsInside.loop((file, _) { + // itemsInside.loop((file){ // if (file is File) { // // -- file stats inside each dir // final stats = file.statSync(); @@ -608,7 +621,7 @@ class _NamidaFileBrowserState extends State<_NamidaF final extensions = params.allowedExtensions; if (excludeHidden && extensions.isNotEmpty) { - items.loop((e, _) { + items.loop((e) { final filename = e.path.split(_pathSeparator).last; if (e is Directory) { if (!filename.startsWith('.')) onAdd(e); @@ -617,12 +630,12 @@ class _NamidaFileBrowserState extends State<_NamidaF } }); } else if (excludeHidden) { - items.loop((e, _) { + items.loop((e) { final fileorDirName = e.path.split(_pathSeparator).last; if (!fileorDirName.startsWith('.')) onAdd(e); }); } else if (extensions.isNotEmpty) { - items.loop((e, _) { + items.loop((e) { if (e is File) { final filename = e.path.split(_pathSeparator).last; if (extensions.any((ext) => filename.endsWith(ext))) onAdd(e); @@ -631,7 +644,7 @@ class _NamidaFileBrowserState extends State<_NamidaF } }); } else { - items.loop((e, _) => onAdd(e)); + items.loop((e) => onAdd(e)); } files.sortBy((e) => _pathToName(e.path)); @@ -721,7 +734,7 @@ class _NamidaFileBrowserState extends State<_NamidaF void _onSelectionComplete(List items) { widget.onSelect.completeIfWasnt(items); - context.safePop(); + widget.onPop(); } final _selectedFiles = []; @@ -945,7 +958,7 @@ class _NamidaFileBrowserState extends State<_NamidaF children: [ IconButton( onPressed: () { - context.safePop(rootNavigator: true); + _onSelectionComplete([]); }, icon: const Icon( Broken.arrow_left_2, @@ -957,7 +970,7 @@ class _NamidaFileBrowserState extends State<_NamidaF ? Text( widget.note.addDQuotation(), style: context.textTheme.displayMedium?.copyWith( - fontSize: 16.0.multipliedFontScale, + fontSize: 16.0, ), ) : const SizedBox(), @@ -1049,9 +1062,9 @@ class _NamidaFileBrowserState extends State<_NamidaF }, icon: Obx( () => Icon( - _showHiddenFiles.value ? Broken.eye : Broken.eye_slash, + _showHiddenFiles.valueR ? Broken.eye : Broken.eye_slash, size: 20.0, - color: _showHiddenFiles.value ? null : context.defaultIconColor(), + color: _showHiddenFiles.valueR ? null : context.defaultIconColor(), ), ), ), @@ -1132,7 +1145,7 @@ class _NamidaFileBrowserState extends State<_NamidaF const Spacer(), Obx( () => SortByMenu( - title: _sortTypeToName[_sortType.value] ?? '', + title: _sortTypeToName[_sortType.valueR] ?? '', popupMenuChild: () { Widget getTile(IconData icon, String title, _SortType sort) { return SmallListTile( @@ -1147,23 +1160,21 @@ class _NamidaFileBrowserState extends State<_NamidaF ); } - return Obx( - () => Column( - children: [ - ListTileWithCheckMark( - active: _sortReversed.value, - onTap: () => _sortItems(null, !_sortReversed.value), - ), - const SizedBox(height: 8.0), - getTile(Broken.text, lang.FILE_NAME, _SortType.name), - getTile(Broken.calendar, lang.DATE, _SortType.dateModified), - getTile(Broken.document_code, lang.EXTENSION, _SortType.type), - getTile(Broken.math, lang.SIZE, _SortType.size), - ], - ), + return Column( + children: [ + ListTileWithCheckMark( + activeRx: _sortReversed, + onTap: () => _sortItems(null, !_sortReversed.value), + ), + const SizedBox(height: 8.0), + getTile(Broken.text, lang.FILE_NAME, _SortType.name), + getTile(Broken.calendar, lang.DATE, _SortType.dateModified), + getTile(Broken.document_code, lang.EXTENSION, _SortType.type), + getTile(Broken.math, lang.SIZE, _SortType.size), + ], ); }, - isCurrentlyReversed: _sortReversed.value, + isCurrentlyReversed: _sortReversed.valueR, onReverseIconTap: () => _sortItems(null, !_sortReversed.value), ), ), @@ -1254,7 +1265,7 @@ class _NamidaFileBrowserState extends State<_NamidaF final info = _currentInfoFiles[file.path]; final image = _getFileImage(file); return _FileSystemChip( - position: index + _currentFolders.length, + position: index + _currentFolders.length + 1, bgColor: chipColor, onTap: () => _onFileTap(file), onLongPress: () => _onFileLongPress(file), @@ -1366,7 +1377,7 @@ class _FileSystemChip extends StatelessWidget { Text( title, style: context.textTheme.displaySmall?.copyWith( - fontSize: 13.0.multipliedFontScale, + fontSize: 13.0, fontWeight: FontWeight.w600, ), ), @@ -1374,9 +1385,9 @@ class _FileSystemChip extends StatelessWidget { Text( subtitle, style: context.textTheme.displaySmall?.copyWith( - fontSize: 12.0.multipliedFontScale, + fontSize: 12.0, fontWeight: FontWeight.w400, - // color: context.theme.colorScheme.onBackground.withOpacity(0.7), + // color: context.theme.colorScheme.onSurface.withOpacity(0.7), ), ), ], diff --git a/lib/controller/folders_controller.dart b/lib/controller/folders_controller.dart index 61cfba76..d00f4305 100644 --- a/lib/controller/folders_controller.dart +++ b/lib/controller/folders_controller.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/folder.dart'; import 'package:namida/class/track.dart'; @@ -18,16 +18,14 @@ class Folders { final RxList currentFolderslist = [].obs; - List get currentTracks => currentFolder.value?.tracks ?? []; - /// Even with this logic, root paths are invincible. - final RxBool isHome = true.obs; + final isHome = true.obs; /// Used for non-hierarchy. - final RxBool isInside = false.obs; + final isInside = false.obs; /// Highlights the track that is meant to be navigated to after calling [goToFolder]. - final RxnInt indexToScrollTo = RxnInt(); + final indexToScrollTo = Rxn(); final _latestScrollOffset = {}; @@ -61,7 +59,7 @@ class Folders { currentFolder.value = folder; if (trackToScrollTo != null) { - indexToScrollTo.value = folder.tracks.indexOf(trackToScrollTo); + indexToScrollTo.value = folder.tracks().indexOf(trackToScrollTo); } _scrollJump(jumpTo); } diff --git a/lib/controller/generators_controller.dart b/lib/controller/generators_controller.dart index 64c76f57..7c5c457b 100644 --- a/lib/controller/generators_controller.dart +++ b/lib/controller/generators_controller.dart @@ -46,7 +46,7 @@ class NamidaGenerator extends NamidaGeneratorBase { // -- [yyyy] year format. if (yearTimeStamp.toString().length == 4) { - allTracksInLibrary.loop((e, index) { + allTracksInLibrary.loop((e) { if (e.year != 0) { // -- if the track also has [yyyy] if (e.year.toString().length == 4) { @@ -69,7 +69,7 @@ class NamidaGenerator extends NamidaGeneratorBase { final dateParsed = DateTime.tryParse(yearTimeStamp.toString()); if (dateParsed == null) return []; - allTracksInLibrary.loop((e, index) { + allTracksInLibrary.loop((e) { if (e.year != 0) { final dt = DateTime.tryParse(e.year.toString()); if (dt != null && (dt.difference(dateParsed).inDays).abs() <= daysRange) { @@ -87,7 +87,7 @@ class NamidaGenerator extends NamidaGeneratorBase { int maxRating, ) { final finalTracks = []; - Indexer.inst.trackStatsMap.forEach((key, value) { + Indexer.inst.trackStatsMap.value.forEach((key, value) { if (value.rating >= minRating && value.rating <= maxRating) { finalTracks.add(key); } diff --git a/lib/controller/history_controller.dart b/lib/controller/history_controller.dart index 93c9f5a6..6a2a2d6c 100644 --- a/lib/controller/history_controller.dart +++ b/lib/controller/history_controller.dart @@ -17,6 +17,15 @@ class HistoryController with HistoryManager { static final HistoryController _instance = HistoryController._internal(); HistoryController._internal(); + @override + double daysToSectionExtent(List days) { + final trackTileExtent = Dimensions.inst.trackTileItemExtent; + const dayHeaderExtent = kHistoryDayHeaderHeightWithPadding; + double total = 0; + days.loop((day) => total += dayToSectionExtent(day, trackTileExtent, dayHeaderExtent)); + return total; + } + Future replaceTracksDirectoryInHistory(String oldDir, String newDir, {Iterable? forThesePathsOnly, bool ensureNewFileExists = false}) async { String getNewPath(String old) => old.replaceFirst(oldDir, newDir); await replaceTheseTracksInHistory( @@ -48,22 +57,17 @@ class HistoryController with HistoryManager { ); } - Future removeSourcesTracksFromHistory(List sources, {DateTime? oldestDate, DateTime? newestDate, bool andSave = true}) async { + Future removeSourcesTracksFromHistory(List sources, {DateTime? oldestDate, DateTime? newestDate}) async { if (sources.isEmpty) return 0; int totalRemoved = 0; - - Future saveHistory([List? daysToSave]) async { - if (andSave) { - await saveHistoryToStorage(daysToSave); - } - } + List? daysToSave; // -- remove all sources (i.e all history) if (oldestDate == null && newestDate == null && sources.isEqualTo(TrackSource.values)) { - totalRemoved = historyTracksLength; + totalRemoved = totalHistoryItemsCount.value; historyMap.value.clear(); - await saveHistory(); + daysToSave = null; } else { final daysToRemoveFrom = historyDays.toList(); @@ -84,20 +88,26 @@ class HistoryController with HistoryManager { // -- will loop the whole days. /* if (oldestDay == null && newestDay == null) {} */ - daysToRemoveFrom.loop((d, index) { - totalRemoved += historyMap.value[d]?.removeWhereWithDifference((twd) => sources.contains(twd.source)) ?? 0; + final history = historyMap.value; + daysToRemoveFrom.loop((d) { + totalRemoved += history[d]?.removeWhereWithDifference((twd) => sources.contains(twd.source)) ?? 0; }); - await saveHistory(daysToRemoveFrom); + daysToSave = daysToRemoveFrom; + } + + if (totalRemoved > 0) { + totalHistoryItemsCount.value -= totalRemoved; + historyMap.refresh(); + updateMostPlayedPlaylist(); + await saveHistoryToStorage(daysToSave); + } else if (daysToSave != null) { + // just in case its edited but `totalRemoved` uh + await saveHistoryToStorage(daysToSave); } - updateMostPlayedPlaylist(); - calculateAllItemsExtentsInHistory(); return totalRemoved; } - @override - double get DAY_HEADER_HEIGHT_WITH_PADDING => kHistoryDayHeaderHeightWithPadding; - @override String get HISTORY_DIRECTORY => AppDirs.HISTORY_PLAYLIST; @@ -108,13 +118,14 @@ class HistoryController with HistoryManager { Track mainItemToSubItem(TrackWithDate item) => item.track; @override - Future<({SplayTreeMap> historyMap, Map> topItems})> prepareAllHistoryFilesFunction(String directoryPath) async { + Future> prepareAllHistoryFilesFunction(String directoryPath) async { return await _readHistoryFilesCompute.thready(directoryPath); } - static Future<({SplayTreeMap> historyMap, Map> topItems})> _readHistoryFilesCompute(String path) async { + static Future> _readHistoryFilesCompute(String path) async { final map = SplayTreeMap>((date1, date2) => date2.compareTo(date1)); final tempMapTopItems = >{}; + int totalCount = 0; for (final f in Directory(path).listSyncSafe()) { if (f is File) { try { @@ -122,8 +133,9 @@ class HistoryController with HistoryManager { final dayOfTrack = int.parse(f.path.getFilenameWOExt); final listTracks = (response as List?)?.mapped((e) => TrackWithDate.fromJson(e)) ?? []; map[dayOfTrack] = listTracks; + totalCount += listTracks.length; - listTracks.loop((e, index) { + listTracks.loop((e) { tempMapTopItems.addForce(e.track, e.dateTimeAdded.millisecondsSinceEpoch); }); } catch (e) { @@ -149,7 +161,11 @@ class HistoryController with HistoryManager { }); final topItems = Map.fromEntries(sortedEntries); - return (historyMap: map, topItems: topItems); + return HistoryPrepareInfo( + historyMap: map, + topItems: topItems, + totalItemsCount: totalCount, + ); } @override @@ -160,7 +176,4 @@ class HistoryController with HistoryManager { @override bool get mostPlayedCustomIsStartOfDay => settings.mostPlayedCustomisStartOfDay.value; - - @override - double get trackTileItemExtent => Dimensions.inst.trackTileItemExtent; } diff --git a/lib/controller/indexer_controller.dart b/lib/controller/indexer_controller.dart index 3b522e4e..bcc3a003 100644 --- a/lib/controller/indexer_controller.dart +++ b/lib/controller/indexer_controller.dart @@ -6,7 +6,7 @@ import 'dart:io'; import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:on_audio_query/on_audio_query.dart'; import 'package:namida/class/faudiomodel.dart'; @@ -37,19 +37,19 @@ class Indexer { bool get _defaultUseMediaStore => settings.useMediaStore.value; - final RxBool isIndexing = false.obs; + final isIndexing = false.obs; - final RxSet allAudioFiles = {}.obs; - final RxInt filteredForSizeDurationTracks = 0.obs; - final RxInt duplicatedTracksLength = 0.obs; - final RxInt tracksExcludedByNoMedia = 0.obs; + final allAudioFiles = {}.obs; + final filteredForSizeDurationTracks = 0.obs; + final duplicatedTracksLength = 0.obs; + final tracksExcludedByNoMedia = 0.obs; - final RxInt artworksInStorage = 0.obs; - final RxInt colorPalettesInStorage = 0.obs; - final RxInt videosInStorage = 0.obs; + final artworksInStorage = 0.obs; + final colorPalettesInStorage = 0.obs; + final videosInStorage = 0.obs; - final RxInt artworksSizeInStorage = 0.obs; - final RxInt videosSizeInStorage = 0.obs; + final artworksSizeInStorage = 0.obs; + final videosSizeInStorage = 0.obs; final mainMapAlbums = LibraryItemMap(); final mainMapArtists = LibraryItemMap(); @@ -73,7 +73,7 @@ class Indexer { late final _audioQuery = OnAudioQuery(); List get recentlyAddedTracks { - final alltracks = List.from(tracksInfoList); + final alltracks = List.from(tracksInfoList.value); alltracks.sortByReverseAlt((e) => e.dateModified, (e) => e.dateAdded); return alltracks; } @@ -204,50 +204,64 @@ class Indexer { /// Adds all tracks inside [tracksInfoList] to their respective album, artist, etc.. /// & sorts all media. void _afterIndexing() { - mainMapAlbums.value.clear(); - mainMapArtists.value.clear(); - mainMapAlbumArtists.value.clear(); - mainMapComposer.value.clear(); - mainMapGenres.value.clear(); - mainMapFolders.clear(); + this.mainMapAlbums.clear(); + this.mainMapArtists.clear(); + this.mainMapAlbumArtists.clear(); + this.mainMapComposer.clear(); + this.mainMapGenres.clear(); + this.mainMapFolders.clear(); + + final mainMapAlbums = this.mainMapAlbums.value; + final mainMapArtists = this.mainMapArtists.value; + final mainMapAlbumArtists = this.mainMapAlbumArtists.value; + final mainMapComposer = this.mainMapComposer.value; + final mainMapGenres = this.mainMapGenres.value; + final mainMapFolders = this.mainMapFolders.value; // --- Sorting All Sublists --- - tracksInfoList.loop((tr, i) { + tracksInfoList.loop((tr) { final trExt = tr.toTrackExt(); // -- Assigning Albums - mainMapAlbums.value.addForce(trExt.albumIdentifier, tr); + mainMapAlbums.addForce(trExt.albumIdentifier, tr); // -- Assigning Artists - trExt.artistsList.loop((artist, i) { - mainMapArtists.value.addForce(artist, tr); + trExt.artistsList.loop((artist) { + mainMapArtists.addForce(artist, tr); }); // -- Assigning Album Artist - mainMapAlbumArtists.value.addForce(trExt.albumArtist, tr); + mainMapAlbumArtists.addForce(trExt.albumArtist, tr); // -- Assigning Composer - mainMapComposer.value.addForce(trExt.composer, tr); + mainMapComposer.addForce(trExt.composer, tr); // -- Assigning Genres - trExt.genresList.loop((genre, i) { - mainMapGenres.value.addForce(genre, tr); + trExt.genresList.loop((genre) { + mainMapGenres.addForce(genre, tr); }); // -- Assigning Folders mainMapFolders.addForce(tr.folder, tr); }); + this.mainMapAlbums.refresh(); + this.mainMapArtists.refresh(); + this.mainMapAlbumArtists.refresh(); + this.mainMapComposer.refresh(); + this.mainMapGenres.refresh(); + this.mainMapFolders.refresh(); + Folders.inst.onMapChanged(mainMapFolders); _sortAll(); sortMediaTracksSubLists(MediaType.values); } void sortMediaTracksSubLists(List medias) { - medias.loop((e, index) { + medias.loop((e) { final sorters = SearchSortController.inst.getMediaTracksSortingComparables(e); void sortPls(Iterable> trs, MediaType type) { - final reverse = settings.mediaItemsTrackSortingReverse[type] ?? false; + final reverse = settings.mediaItemsTrackSortingReverse.value[type] ?? false; if (reverse) { for (final e in trs) { e.sortByReverseAlts(sorters); @@ -308,12 +322,12 @@ class Indexer { final trExt = tr.toTrackExt(); mainMapAlbums.value[trExt.albumIdentifier]?.remove(tr); - trExt.artistsList.loop((artist, i) { + trExt.artistsList.loop((artist) { mainMapArtists.value[artist]?.remove(tr); }); mainMapAlbumArtists.value[trExt.albumArtist]?.remove(tr); mainMapComposer.value[trExt.composer]?.remove(tr); - trExt.genresList.loop((genre, i) { + trExt.genresList.loop((genre) { mainMapGenres.value[genre]?.remove(tr); }); mainMapFolders[tr.folder]?.remove(tr); @@ -322,6 +336,13 @@ class Indexer { } void _addTheseTracksToAlbumGenreArtistEtc(List tracks) { + final mainMapAlbums = this.mainMapAlbums.value; + final mainMapArtists = this.mainMapArtists.value; + final mainMapAlbumArtists = this.mainMapAlbumArtists.value; + final mainMapComposer = this.mainMapComposer.value; + final mainMapGenres = this.mainMapGenres.value; + final mainMapFolders = this.mainMapFolders.value; + final List addedAlbums = []; final List addedArtists = []; final List addedAlbumArtists = []; @@ -329,22 +350,22 @@ class Indexer { final List addedGenres = []; final List addedFolders = []; - tracks.loop((tr, _) { + tracks.loop((tr) { final trExt = tr.toTrackExt(); // -- Assigning Albums - mainMapAlbums.value.addNoDuplicatesForce(trExt.albumIdentifier, tr); + mainMapAlbums.addNoDuplicatesForce(trExt.albumIdentifier, tr); // -- Assigning Artists - trExt.artistsList.loop((artist, i) { - mainMapArtists.value.addNoDuplicatesForce(artist, tr); + trExt.artistsList.loop((artist) { + mainMapArtists.addNoDuplicatesForce(artist, tr); }); - mainMapAlbumArtists.value.addNoDuplicatesForce(trExt.albumArtist, tr); - mainMapComposer.value.addNoDuplicatesForce(trExt.composer, tr); + mainMapAlbumArtists.addNoDuplicatesForce(trExt.albumArtist, tr); + mainMapComposer.addNoDuplicatesForce(trExt.composer, tr); // -- Assigning Genres - trExt.genresList.loop((genre, i) { - mainMapGenres.value.addNoDuplicatesForce(genre, tr); + trExt.genresList.loop((genre) { + mainMapGenres.addNoDuplicatesForce(genre, tr); }); // -- Assigning Folders @@ -363,36 +384,26 @@ class Indexer { final artistSorters = SearchSortController.inst.getMediaTracksSortingComparables(MediaType.artist); final genreSorters = SearchSortController.inst.getMediaTracksSortingComparables(MediaType.genre); final folderSorters = SearchSortController.inst.getMediaTracksSortingComparables(MediaType.folder); - addedAlbums - ..removeDuplicates() - ..loop((e, index) { - mainMapAlbums.value[e]?.sortByAlts(albumSorters); - }); - addedArtists - ..removeDuplicates() - ..loop((e, index) { - mainMapArtists.value[e]?.sortByAlts(artistSorters); - }); - addedAlbumArtists - ..removeDuplicates() - ..loop((e, index) { - mainMapAlbumArtists.value[e]?.sortByAlts(artistSorters); - }); - addedComposers - ..removeDuplicates() - ..loop((e, index) { - mainMapComposer.value[e]?.sortByAlts(artistSorters); - }); - addedGenres - ..removeDuplicates() - ..loop((e, index) { - mainMapGenres.value[e]?.sortByAlts(genreSorters); - }); - addedFolders - ..removeDuplicates() - ..loop((e, index) { - mainMapFolders[e]?.sortByAlts(folderSorters); - }); + + void cleanyLoopy(MediaType type, List added, Map> map, List Function(T tr)> sorters) { + if (added.isEmpty) return; + added.removeDuplicates(); + if (added.isEmpty) return; + + final reverse = settings.mediaItemsTrackSortingReverse.value[type] ?? false; + if (reverse) { + added.loop((e) => map[e]?.sortByReverseAlts(sorters)); + } else { + added.loop((e) => map[e]?.sortByAlts(sorters)); + } + } + + cleanyLoopy(MediaType.album, addedAlbums, mainMapAlbums, albumSorters); + cleanyLoopy(MediaType.artist, addedArtists, mainMapArtists, artistSorters); + cleanyLoopy(MediaType.albumArtist, addedAlbumArtists, mainMapAlbumArtists, artistSorters); + cleanyLoopy(MediaType.composer, addedComposers, mainMapComposer, artistSorters); + cleanyLoopy(MediaType.genre, addedGenres, mainMapGenres, genreSorters); + cleanyLoopy(MediaType.folder, addedFolders, mainMapFolders, folderSorters); Folders.inst.onMapChanged(mainMapFolders); _sortAll(); @@ -623,10 +634,10 @@ class Indexer { final tracksReal = []; final tracksRealPaths = []; final tracksMissing = []; - await tracks.loopFuture((tr, index) async { + tracks.loop((tr) { bool exists = false; try { - exists = await File(tr.path).exists(); + exists = File(tr.path).existsSync(); } catch (_) {} if (exists) { tracksReal.add(tr); @@ -641,7 +652,7 @@ class Indexer { imageCache.clearLiveImages(); } - tracksMissing.loop((e, _) => onProgress(false)); + tracksMissing.loop((e) => onProgress(false)); final stream = await FAudioTaggerController.inst.extractMetadataAsStream( paths: tracksRealPaths, @@ -670,7 +681,7 @@ class Indexer { } final finalTrack = []; - tracksReal.loop((p, index) { + tracksReal.loop((p) { final tr = p.path.toTrackOrNull(); if (tr != null) finalTrack.add(tr); }); @@ -713,7 +724,7 @@ class Indexer { CurrentColor.inst.reExtractTrackColorPalette(track: ot, newNC: null, imagePath: ot.pathToImage); } } - oldTracks.loop((tr, index) => _removeThisTrackFromAlbumGenreArtistEtc(tr)); + oldTracks.loop((tr) => _removeThisTrackFromAlbumGenreArtistEtc(tr)); _addTheseTracksToAlbumGenreArtistEtc(newTracks); await _sortAndSaveTracks(); } @@ -820,7 +831,7 @@ class Indexer { allTracksMappedByPath.clear(); allTracksMappedByYTID.clear(); _currentFileNamesMap.clear(); - trs.loop((e, _) => _addTrackToLists(e.$1, false, null)); + trs.loop((e) => _addTrackToLists(e.$1, false, null)); } else { FAudioTaggerController.inst.currentPathsBeingExtracted.clear(); final audioFilesWithoutDuplicates = []; @@ -858,7 +869,7 @@ class Indexer { ); } - audioFilesParts.loop((part, partIndex) { + audioFilesParts.loopAdv((part, partIndex) { extractFunction(part).then((value) => audioFilesCompleters[partIndex].complete()); }); await Future.wait(audioFilesCompleters.map((e) => e.future).toList()); @@ -881,7 +892,7 @@ class Indexer { Future _saveTrackFileToStorage() async { TrackTileManager.onTrackItemPropChange(); - await File(AppPaths.TRACKS).writeAsJson(tracksInfoList.map((key) => allTracksMappedByPath[key]?.toJson()).toList()); + await File(AppPaths.TRACKS).writeAsJson(tracksInfoList.value.map((key) => allTracksMappedByPath[key]?.toJson()).toList()); } Future updateTrackDuration(Track track, Duration? dur) async { @@ -983,8 +994,8 @@ class Indexer { static List splitBySeparators(String? string, Iterable separators, String fallback, Iterable blacklist) { final List finalStrings = []; final List pre = string?.trimAll().multiSplit(separators, blacklist) ?? [fallback]; - pre.loop((e, index) { - finalStrings.addIf(e != '', e.trimAll()); + pre.loop((e) { + if (e != '') finalStrings.add(e.trimAll()); }); return finalStrings; } @@ -1058,8 +1069,8 @@ class Indexer { return (cleanedUpTitle, cleanedUpArtist); } - Set getNewFoundPaths(Set currentFiles) => currentFiles.difference(Set.of(tracksInfoList.map((t) => t.path))); - Set getDeletedPaths(Set currentFiles) => Set.of(tracksInfoList.map((t) => t.path)).difference(currentFiles); + Set getNewFoundPaths(Set currentFiles) => currentFiles.difference(Set.of(tracksInfoList.value.map((t) => t.path))); + Set getDeletedPaths(Set currentFiles) => Set.of(tracksInfoList.value.map((t) => t.path)).difference(currentFiles); /// [strictNoMedia] forces all subdirectories to follow the same result of the parent. /// @@ -1071,7 +1082,7 @@ class Indexer { final parameters = { 'allAvailableDirectories': allAvailableDirectories, - 'directoriesToExclude': settings.directoriesToExclude.toList(), + 'directoriesToExclude': settings.directoriesToExclude.value, 'extensions': kAudioFileExtensions, 'imageExtensions': kImageFilesExtensions, }; @@ -1102,7 +1113,7 @@ class Indexer { _lastAvailableDirectories = null; // for when forceReCheck enabled. _lastAvailableDirectories = Completer>(); final parameters = { - 'directoriesToScan': settings.directoriesToScan.toList(), + 'directoriesToScan': settings.directoriesToScan.value, 'respectNoMedia': settings.respectNoMedia.value, 'strictNoMedia': strictNoMedia, // TODO: expose [strictNoMedia] in settings? }; @@ -1155,10 +1166,10 @@ class Indexer { Future> _fetchMediaStoreTracks() async { final allMusic = await _audioQuery.querySongs(); // -- folders selected will be ignored when [settings.useMediaStore.value] is enabled. - allMusic.retainWhere( - (element) => settings.directoriesToExclude.every((dir) => !element.data.startsWith(dir)) /* && settings.directoriesToScan.any((dir) => element.data.startsWith(dir)) */); + allMusic.retainWhere((element) => + settings.directoriesToExclude.value.every((dir) => !element.data.startsWith(dir)) /* && settings.directoriesToScan.any((dir) => element.data.startsWith(dir)) */); final tracks = <(TrackExtended, int)>[]; - allMusic.loop((e, _) { + allMusic.loop((e) { final map = e.getMap; final artist = e.artist; final artists = artist == null @@ -1263,7 +1274,7 @@ class Indexer { await _updateDirectoryStats(AppDirs.VIDEOS_CACHE, videosInStorage, videosSizeInStorage); } - Future _updateDirectoryStats(String dirPath, RxInt? filesCountVariable, RxInt? filesSizeVariable) async { + Future _updateDirectoryStats(String dirPath, Rx? filesCountVariable, Rx? filesSizeVariable) async { // resets values filesCountVariable?.value = 0; filesSizeVariable?.value = 0; diff --git a/lib/controller/json_to_history_parser.dart b/lib/controller/json_to_history_parser.dart index 0f32f877..bbd62486 100644 --- a/lib/controller/json_to_history_parser.dart +++ b/lib/controller/json_to_history_parser.dart @@ -1,3 +1,4 @@ +// ignore_for_file: avoid_rx_value_getter_outside_obx import 'dart:async'; import 'dart:collection'; import 'dart:convert'; @@ -5,7 +6,6 @@ import 'dart:io'; import 'dart:isolate'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:namida/class/split_config.dart'; @@ -20,6 +20,7 @@ import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/dialogs/track_advanced_dialog.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/class/youtube_id.dart'; @@ -31,22 +32,22 @@ class JsonToHistoryParser { static final JsonToHistoryParser _instance = JsonToHistoryParser._internal(); JsonToHistoryParser._internal(); - final RxInt parsedHistoryJson = 0.obs; - final RxInt totalJsonToParse = 0.obs; - final RxInt addedHistoryJsonToPlaylist = 0.obs; - final RxBool isParsing = false.obs; - final RxBool isLoadingFile = false.obs; - final RxInt _updatingYoutubeStatsDirectoryProgress = 0.obs; - final RxInt _updatingYoutubeStatsDirectoryTotal = 0.obs; + final parsedHistoryJson = 0.obs; + final totalJsonToParse = 0.obs; + final addedHistoryJsonToPlaylist = 0.obs; + final isParsing = false.obs; + final isLoadingFile = false.obs; + final _updatingYoutubeStatsDirectoryProgress = 0.obs; + final _updatingYoutubeStatsDirectoryTotal = 0.obs; final Rx currentParsingSource = TrackSource.local.obs; final _currentOldestDate = Rxn(); final _currentNewestDate = Rxn(); - String get parsedProgress => '${parsedHistoryJson.value.formatDecimal()} / ${totalJsonToParse.value.formatDecimal()}'; - String get parsedProgressPercentage => '${(_percentage * 100).round()}%'; - String get addedHistoryJson => addedHistoryJsonToPlaylist.value.formatDecimal(); - double get _percentage { - final p = parsedHistoryJson.value / totalJsonToParse.value; + String get _parsedProgressR => '${parsedHistoryJson.valueR.formatDecimal()} / ${totalJsonToParse.valueR.formatDecimal()}'; + String get _parsedProgressPercentageR => '${(_percentageR * 100).round()}%'; + String get _addedHistoryJsonR => addedHistoryJsonToPlaylist.valueR.formatDecimal(); + double get _percentageR { + final p = parsedHistoryJson.valueR / totalJsonToParse.valueR; return p.isFinite ? p : 0; } @@ -57,7 +58,7 @@ class JsonToHistoryParser { void showParsingProgressDialog() { if (_isShowingParsingMenu) return; Widget getTextWidget(String text, {TextStyle? style}) { - return Text(text, style: style ?? Get.textTheme.displayMedium); + return Text(text, style: style ?? namida.textTheme.displayMedium); } _isShowingParsingMenu = true; @@ -71,16 +72,16 @@ class JsonToHistoryParser { normalTitleStyle: true, titleWidgetInPadding: Obx( () { - final title = '${isParsing.value ? lang.EXTRACTING_INFO : lang.DONE} ($parsedProgressPercentage)'; + final title = '${isParsing.valueR ? lang.EXTRACTING_INFO : lang.DONE} ($_parsedProgressPercentageR)'; return Text( - "$title ${isParsing.value ? '' : ' ✓'}", - style: Get.textTheme.displayLarge, + "$title ${isParsing.valueR ? '' : ' ✓'}", + style: namida.textTheme.displayLarge, ); }, ), actions: [ TextButton( - child: Text(lang.CONFIRM), + child: NamidaButtonText(lang.CONFIRM), onPressed: () { _hideParsingDialog(); NamidaNavigator.inst.closeDialog(); @@ -92,20 +93,22 @@ class JsonToHistoryParser { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Obx(() => getTextWidget('${lang.LOADING_FILE}... ${isLoadingFile.value ? '' : lang.DONE}')), + Obx(() => getTextWidget('${lang.LOADING_FILE}... ${isLoadingFile.valueR ? '' : lang.DONE}')), const SizedBox(height: 10.0), - Obx(() => getTextWidget('$parsedProgress ${lang.PARSED}')), + Obx(() => getTextWidget('$_parsedProgressR ${lang.PARSED}')), const SizedBox(height: 10.0), - Obx(() => getTextWidget('$addedHistoryJson ${lang.ADDED}')), + Obx(() => getTextWidget('$_addedHistoryJsonR ${lang.ADDED}')), const SizedBox(height: 4.0), if (dateText != '') ...[ - getTextWidget(dateText, style: Get.textTheme.displaySmall), + getTextWidget(dateText, style: namida.textTheme.displaySmall), const SizedBox(height: 4.0), ], const SizedBox(height: 4.0), Obx(() { - final shouldShow = currentParsingSource.value == TrackSource.youtube || currentParsingSource.value == TrackSource.youtubeMusic; - return shouldShow ? getTextWidget('${lang.STATS}: ${_updatingYoutubeStatsDirectoryProgress.value}/${_updatingYoutubeStatsDirectoryTotal.value}') : const SizedBox(); + final shouldShow = currentParsingSource.valueR == TrackSource.youtube || currentParsingSource.valueR == TrackSource.youtubeMusic; + return shouldShow + ? getTextWidget('${lang.STATS}: ${_updatingYoutubeStatsDirectoryProgress.valueR}/${_updatingYoutubeStatsDirectoryTotal.valueR}') + : const SizedBox(); }), ], ), @@ -114,12 +117,12 @@ class JsonToHistoryParser { ); } - bool get shouldShowMissingEntriesDialog => _latestMissingMap.isNotEmpty && _latestMissingMap.length != _latestMissingMapAddedStatus.length; + bool get shouldShowMissingEntriesDialog => _latestMissingMap.valueR.isNotEmpty && _latestMissingMap.length != _latestMissingMapAddedStatus.length; final _latestMissingMap = <_MissingListenEntry, List>{}.obs; final _latestMissingMapAddedStatus = <_MissingListenEntry, Track>{}.obs; void showMissingEntriesDialog() { - if (_latestMissingMap.isEmpty) return; + if (_latestMissingMap.value.isEmpty) return; Future addTrackToHistory(MapEntry<_MissingListenEntry, List> entry, Track choosen) async { final twds = entry.value.map( @@ -200,10 +203,10 @@ class JsonToHistoryParser { iconSize: 24.0, onPressed: () async { confirmAddAsDummy( - confirmMessage: 'Add ${_latestMissingMap.entries.length} as dummy tracks?', + confirmMessage: 'Add ${_latestMissingMap.value.entries.length} as dummy tracks?', onConfirm: () async { final historyDays = []; - final missing = _latestMissingMap.entries.toList()..sortByReverse((e) => e.value.length); + final missing = _latestMissingMap.value.entries.toList()..sortByReverse((e) => e.value.length); for (final e in missing) { final replacedWithTrack = _latestMissingMapAddedStatus[e.key]; if (replacedWithTrack == null) { @@ -220,21 +223,21 @@ class JsonToHistoryParser { }, ); }, - ).animateEntrance(showWhen: showAddAsDummyIcon.value), + ).animateEntrance(showWhen: showAddAsDummyIcon.valueR), ), const SizedBox(width: 8.0), NamidaIconButton( horizontalPadding: 0.0, icon: Broken.eye, onPressed: () { - showAddAsDummyIcon.value = !showAddAsDummyIcon.value; + showAddAsDummyIcon.toggle(); }, ), const SizedBox(width: 8.0), ], child: SizedBox( - width: Get.width, - height: Get.height * 0.6, + width: namida.width, + height: namida.height * 0.6, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -242,13 +245,13 @@ class JsonToHistoryParser { padding: const EdgeInsets.all(8.0), child: Text( lang.HISTORY_IMPORT_MISSING_ENTRIES_NOTE, - style: Get.textTheme.displaySmall, + style: namida.textTheme.displaySmall, ), ), Expanded( child: Obx( () { - final missing = _latestMissingMap.entries.toList()..sortByReverse((e) => e.value.length); + final missing = _latestMissingMap.valueR.entries.toList()..sortByReverse((e) => e.value.length); return NamidaScrollbarWithController( child: (sc) => ListView.separated( controller: sc, @@ -266,19 +269,19 @@ class JsonToHistoryParser { opacity: replacedWithTrack != null ? 0.6 : 1.0, child: NamidaInkWell( onTap: () => pickTrack(entry), - bgColor: Get.theme.cardTheme.color, + bgColor: namida.theme.cardTheme.color, borderRadius: 12.0, padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 6.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ NamidaInkWell( - bgColor: Get.theme.cardTheme.color, + bgColor: namida.theme.cardTheme.color, borderRadius: 42.0, padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 2.0), child: Text( entry.value.length.formatDecimal(), - style: Get.textTheme.displaySmall, + style: namida.textTheme.displaySmall, ), ), const SizedBox(width: 12.0), @@ -289,14 +292,14 @@ class JsonToHistoryParser { children: [ Text( entry.key.title, - style: Get.textTheme.displayMedium, + style: namida.textTheme.displayMedium, ), Text( "${entry.key.artistOrChannel} - ${entry.key.source.convertToString}", maxLines: 1, softWrap: false, overflow: TextOverflow.ellipsis, - style: Get.textTheme.displaySmall, + style: namida.textTheme.displaySmall, ), if (replacedWithTrack != null) Text( @@ -304,7 +307,7 @@ class JsonToHistoryParser { maxLines: 2, softWrap: false, overflow: TextOverflow.ellipsis, - style: Get.textTheme.displaySmall?.copyWith(fontSize: 11.5.multipliedFontScale), + style: namida.textTheme.displaySmall?.copyWith(fontSize: 11.5), ), ], ), @@ -326,7 +329,7 @@ class JsonToHistoryParser { confirmMessage: 'Add "${entry.key.artistOrChannel} - ${entry.key.title}" as dummy track?', onConfirm: () async => await addTrackToHistory(entry, getDummyTrack(entry.key)), ), - ).animateEntrance(showWhen: showAddAsDummyIcon.value), + ).animateEntrance(showWhen: showAddAsDummyIcon.valueR), ), const SizedBox(width: 4.0), ], @@ -464,7 +467,7 @@ class JsonToHistoryParser { final portLoadingProgress = ReceivePort(); final params = { - 'tracks': Indexer.inst.allTracksMappedByPath.values + 'tracks': Indexer.inst.allTracksMappedByPath.value.values .map((e) => { 'title': e.title, 'album': e.album, @@ -548,8 +551,6 @@ class JsonToHistoryParser { } /// Returns [daysToSave] to be used by [sortHistoryTracks] && [saveHistoryToStorage]. - /// - /// The first one is for normal history, the second is for youtube history. static ({ Map? affectedIds, List daysToSaveLocal, @@ -579,7 +580,7 @@ class JsonToHistoryParser { Map>? tracksIdsMap; if (isMatchingTypeLink) { tracksIdsMap = >{}; - allTracks.loop((trMap, index) { + allTracks.loop((trMap) { final comment = trMap['comment'] as String; final filename = trMap['filename'] as String; String? link = comment.isEmpty ? null : NamidaLinkRegex.youtubeLinkRegex.firstMatch(comment)?[0]; @@ -643,19 +644,19 @@ class JsonToHistoryParser { matchAll: matchAll, tracksIdsMap: tracksIdsMap, matchByTitleAndArtistIfNotFoundInMap: isMatchingTypeTitleAndArtist, - onMissingEntries: (e) => e.loop((e, index) => missingEntries.addForce(e, e.dateMSSE)), + onMissingEntries: (e) => e.loop((e) => missingEntries.addForce(e, e.dateMSSE)), allTracks: allTracks, artistsSplitConfig: artistsSplitConfig, ); totalAdded += tracks.length; - tracks.loop((item, _) { + tracks.loop((item) { final day = item.dateTimeAdded.toDaysSince1970(); daysToSaveLocal.add(day); localHistory.insertForce(0, day, item); }); // -- youtube history -- - yth.watches.loop((w, index) { + yth.watches.loop((w) { final canAdd = _canSafelyAddToYTHistory( watch: w, matchYT: matchYT, @@ -775,7 +776,7 @@ class JsonToHistoryParser { final tracksToAdd = []; if (tracks.isNotEmpty) { - vh.watches.loop((d, index) { + vh.watches.loop((d) { final canAdd = _canSafelyAddToYTHistory( watch: d, matchYT: matchYT, @@ -821,7 +822,7 @@ class JsonToHistoryParser { final portLoadingProgress = ReceivePort(); final params = { - 'tracks': Indexer.inst.allTracksMappedByPath.values + 'tracks': Indexer.inst.allTracksMappedByPath.value.values .map((e) => { 'title': e.title, 'artist': e.originalArtist, diff --git a/lib/controller/lyrics_controller.dart b/lib/controller/lyrics_controller.dart index 7957bbf7..d97f7dd8 100644 --- a/lib/controller/lyrics_controller.dart +++ b/lib/controller/lyrics_controller.dart @@ -1,4 +1,5 @@ /// copyright: google search request is originally from [@netlob](https://github.com/netlob/dart-lyrics), edited to fit Namida. +library; // ignore_for_file: depend_on_referenced_packages @@ -8,9 +9,10 @@ import 'dart:io'; import 'dart:isolate'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:http/http.dart' as http; import 'package:lrc/lrc.dart'; +import 'package:namida/youtube/class/youtube_id.dart'; import 'package:path/path.dart' as p; import 'package:namida/base/ports_provider.dart'; @@ -38,7 +40,7 @@ class Lyrics { final currentLyricsLRC = Rxn(); final lyricsCanBeAvailable = true.obs; - Track? _currentTrack; + Playable? _currentItem; bool get _lyricsEnabled => settings.enableLyrics.value; bool get _lyricsPrioritizeEmbedded => settings.prioritizeEmbeddedLyrics.value; @@ -53,16 +55,16 @@ class Lyrics { } void resetLyrics() { - _currentTrack = null; + _currentItem = null; currentLyricsText.value = ''; currentLyricsLRC.value = null; _updateWidgets(null); } - Future updateLyrics(Track track) async { + Future updateLyrics(Playable item) async { resetLyrics(); - _currentTrack = track; - bool checkInterrupted() => _currentTrack != track; + _currentItem = item; + bool checkInterrupted() => _currentItem != item; try { textScrollController.jumpTo(0); @@ -72,50 +74,56 @@ class Lyrics { lyricsCanBeAvailable.value = true; if (!_lyricsEnabled) return; - final embedded = track.lyrics; + if (item is YoutubeID) { + // TODO: allow lyrics for youtube videos + } else if (item is Selectable) { + final track = item.track; - if (_lyricsPrioritizeEmbedded && embedded != '') { - final lrc = embedded.parseLRC(); - if (lrc != null && lrc.lyrics.isNotEmpty) { - currentLyricsLRC.value = lrc; - _updateWidgets(lrc); - } else { - currentLyricsText.value = embedded; + final embedded = track.lyrics; + + if (_lyricsPrioritizeEmbedded && embedded != '') { + final lrc = embedded.parseLRC(); + if (lrc != null && lrc.lyrics.isNotEmpty) { + currentLyricsLRC.value = lrc; + _updateWidgets(lrc); + } else { + currentLyricsText.value = embedded; + } + return; } - return; - } - /// 1. device lrc - /// 2. cached lrc - /// 3. track embedded lrc - /// 4. database. - final lrcLyrics = await _fetchLRCBasedLyrics(track, embedded, _lyricsSource); - - if (checkInterrupted()) return; - - if (lrcLyrics.$1 != null) { - currentLyricsLRC.value = lrcLyrics.$1; - _updateWidgets(lrcLyrics.$1); - return; - } else if (lrcLyrics.$2 != null) { - currentLyricsText.value = lrcLyrics.$2 ?? ''; - _updateWidgets(null); - return; - } + /// 1. device lrc + /// 2. cached lrc + /// 3. track embedded lrc + /// 4. database. + final lrcLyrics = await _fetchLRCBasedLyrics(track, embedded, _lyricsSource); - if (checkInterrupted()) return; + if (checkInterrupted()) return; - /// 1. cached txt lyrics - /// 2. track embedded txt - /// 3. google search - final textLyrics = await _fetchTextBasedLyrics(track, embedded, _lyricsSource); + if (lrcLyrics.$1 != null) { + currentLyricsLRC.value = lrcLyrics.$1; + _updateWidgets(lrcLyrics.$1); + return; + } else if (lrcLyrics.$2 != null) { + currentLyricsText.value = lrcLyrics.$2 ?? ''; + _updateWidgets(null); + return; + } - if (checkInterrupted()) return; + if (checkInterrupted()) return; - if (textLyrics != '') { - currentLyricsText.value = textLyrics; - } else { - lyricsCanBeAvailable.value = false; + /// 1. cached txt lyrics + /// 2. track embedded txt + /// 3. google search + final textLyrics = await _fetchTextBasedLyrics(track, embedded, _lyricsSource); + + if (checkInterrupted()) return; + + if (textLyrics != '') { + currentLyricsText.value = textLyrics; + } else { + lyricsCanBeAvailable.value = false; + } } } diff --git a/lib/controller/miniplayer_controller.dart b/lib/controller/miniplayer_controller.dart index 95a565e0..c0e2efbd 100644 --- a/lib/controller/miniplayer_controller.dart +++ b/lib/controller/miniplayer_controller.dart @@ -5,7 +5,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/controller/navigator_controller.dart'; import 'package:namida/controller/player_controller.dart'; @@ -16,6 +16,7 @@ import 'package:namida/core/dimensions.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/packages/mp.dart'; +import 'package:namida/youtube/class/youtube_id.dart'; class MiniPlayerController { static MiniPlayerController get inst => _instance; @@ -30,10 +31,10 @@ class MiniPlayerController { bool get isInQueue => animation.value > 1.0; /// Used to temporarily hold the seek value. - final RxInt seekValue = 0.obs; + final seekValue = 0.obs; /// Indicates that play/pause button is currently pressed. - final RxBool isPlayPauseButtonHighlighted = false.obs; + final isPlayPauseButtonHighlighted = false.obs; /// Prevents Listener while reorderding or dismissing items inside queue. bool isReorderingQueue = false; @@ -42,9 +43,9 @@ class MiniPlayerController { Completer? reorderingQueueCompleterPlayer; /// Icon that represents the direction of the current track - final Rx arrowIcon = Broken.cd.obs; + final arrowIcon = Broken.cd.obso; - final ScrollController queueScrollController = ScrollController(); + late final ScrollController queueScrollController = ScrollController()..addListener(_updateIcon); Future _onMiniplayerDismiss() async => await Player.inst.clearQueue(); @@ -139,40 +140,41 @@ class MiniPlayerController { bool bounceUp = false; bool bounceDown = false; - double get _currentItemExtent => Player.inst.currentQueueYoutube.isNotEmpty ? Dimensions.youtubeCardItemExtent : Dimensions.inst.trackTileItemExtent; + double get _currentItemExtent => Player.inst.currentItem is YoutubeID ? Dimensions.youtubeCardItemExtent : Dimensions.inst.trackTileItemExtent; void animateQueueToCurrentTrack({bool jump = false, bool minZero = false}) { if (queueScrollController.hasClients) { - final trackTileItemScrollOffsetInQueue = _currentItemExtent * Player.inst.currentIndex - screenSize.height * 0.3; + final trackTileItemScrollOffsetInQueue = _currentItemExtent * Player.inst.currentIndex.value - screenSize.height * 0.3; if (queueScrollController.positions.lastOrNull?.pixels == trackTileItemScrollOffsetInQueue) { return; } final finalOffset = minZero ? trackTileItemScrollOffsetInQueue.withMinimum(0) : trackTileItemScrollOffsetInQueue; - if (jump) { - queueScrollController.jumpTo(finalOffset); - } else { - queueScrollController.animateToEff( - finalOffset, - duration: const Duration(milliseconds: 600), - curve: Curves.fastEaseInToSlowEaseOut, - ); - } + try { + if (jump) { + queueScrollController.jumpTo(finalOffset); + } else { + queueScrollController.animateToEff( + finalOffset, + duration: const Duration(milliseconds: 600), + curve: Curves.fastEaseInToSlowEaseOut, + ); + } + } catch (_) {} } } - Future onWillPop() async { - bool val = true; + bool onWillPop() { if (_offset > maxOffset) { // -- isQueue snapToExpanded(); - val = false; + return false; } else if (_offset == maxOffset) { // -- isExpanded snapToMini(); - val = false; + return false; } - return val; + return true; } void onPointerDown(PointerDownEvent event) { @@ -308,8 +310,6 @@ class MiniPlayerController { } } - queueScrollController.removeListener(() {}); - if (shouldSnapToExpanded) { snapToExpanded(); } else { @@ -335,27 +335,21 @@ class MiniPlayerController { if (_maintainStatusBarShowing) NamidaNavigator.inst.setDefaultSystemUI(); } - void _updateScrollPositionInQueue() { - void updateIcon() { - final sizeInSettings = _currentItemExtent * Player.inst.currentIndex - maxOffset * 0.3; - double pixels; - try { - pixels = queueScrollController.position.pixels; - } catch (_) { - pixels = sizeInSettings; - } - if (pixels > sizeInSettings) { - arrowIcon.value = Broken.arrow_up_1; - } else if (pixels < sizeInSettings) { - arrowIcon.value = Broken.arrow_down; - } else if (pixels == sizeInSettings) { - arrowIcon.value = Broken.cd; - } + void _updateIcon() { + final sizeInSettings = _currentItemExtent * Player.inst.currentIndex.value - maxOffset * 0.3; + double pixels; + try { + pixels = queueScrollController.positions.first.pixels; + } catch (_) { + pixels = sizeInSettings; + } + if (pixels > sizeInSettings) { + arrowIcon.value = Broken.arrow_up_1; + } else if (pixels < sizeInSettings) { + arrowIcon.value = Broken.arrow_down; + } else if (pixels == sizeInSettings) { + arrowIcon.value = Broken.cd; } - - animateQueueToCurrentTrack(jump: true); - updateIcon(); - queueScrollController.addListener(updateIcon); } Future snapToQueue({bool animateScrollController = true, bool haptic = true}) async { @@ -367,7 +361,7 @@ class MiniPlayerController { if (animateScrollController) { // updating scroll before snapping makes a nice effect. SchedulerBinding.instance.addPostFrameCallback((timeStamp) { - _updateScrollPositionInQueue(); + animateQueueToCurrentTrack(jump: true); }); } await _snap(haptic: haptic, curve: _bouncingCurveSoft); diff --git a/lib/controller/namida_channel.dart b/lib/controller/namida_channel.dart index 2d50ce4a..e145fbdd 100644 --- a/lib/controller/namida_channel.dart +++ b/lib/controller/namida_channel.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; @@ -55,7 +55,7 @@ class NamidaChannel { Future setMusicAs({required String path, required List types}) async { final t = []; - types.loop((e, index) { + types.loop((e) { final n = _setMusicAsActionConverter[e]; if (n != null) t.add(n); }); diff --git a/lib/controller/navigator_controller.dart b/lib/controller/navigator_controller.dart index 4f145d2f..d6c17d47 100644 --- a/lib/controller/navigator_controller.dart +++ b/lib/controller/navigator_controller.dart @@ -1,12 +1,13 @@ import 'dart:async'; +import 'package:flutter/material.dart' as material; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; import 'package:namida/class/route.dart'; import 'package:namida/controller/folders_controller.dart'; import 'package:namida/controller/miniplayer_controller.dart'; +import 'package:namida/controller/playlist_controller.dart'; import 'package:namida/controller/scroll_search_controller.dart'; import 'package:namida/controller/settings_controller.dart'; import 'package:namida/controller/settings_search_controller.dart'; @@ -19,8 +20,10 @@ import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/themes.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/inner_drawer.dart'; +import 'package:namida/youtube/controller/youtube_playlist_controller.dart'; import 'package:namida/youtube/widgets/yt_queue_chip.dart'; class NamidaNavigator { @@ -28,18 +31,22 @@ class NamidaNavigator { static final NamidaNavigator _instance = NamidaNavigator._internal(); NamidaNavigator._internal(); - final navKey = Get.nestedKey(1); + GlobalKey get _rootNav => namida.rootNavigatorKey; - final ytLocalSearchNavigatorKey = Get.nestedKey(9); + final navKey = GlobalKey(); - final ytMiniplayerCommentsPageKey = Get.nestedKey(11); + final ytLocalSearchNavigatorKey = GlobalKey(); + + final ytMiniplayerCommentsPageKey = GlobalKey(); bool isytLocalSearchInFullPage = false; bool isInYTCommentsSubpage = false; bool isQueueSheetOpen = false; - final RxList currentWidgetStack = [].obs; - NamidaRoute? get currentRoute => currentWidgetStack.lastOrNull; + final currentWidgetStack = [].obs; + NamidaRoute? get currentRoute => currentWidgetStack.value.lastOrNull; + // ignore: avoid_rx_value_getter_outside_obx + NamidaRoute? get currentRouteR => currentWidgetStack.valueR.lastOrNull; int _currentDialogNumber = 0; int _currentMenusNumber = 0; @@ -68,27 +75,34 @@ class NamidaNavigator { _onLandscapeEvents.remove(key); } - Future showMenu(Future? menuFunction) async { + Future showMenu({ + required BuildContext context, + required RelativeRect position, + required List> items, + }) async { _currentMenusNumber++; _printMenus(); - return await menuFunction; + return material.showMenu( + useRootNavigator: true, + context: context, + position: position, + items: items, + ); } void popMenu({bool handleClosing = true}) { if (_currentMenusNumber > 0) { _currentMenusNumber--; if (handleClosing) { - Get.close(1); + _rootNav.currentState?.pop(); } } _printMenus(); } void popAllMenus() { - if (_currentMenusNumber > 0) { - Get.until((route) => route.isFirst); - _currentMenusNumber = 0; - } + _rootNav.currentState?.popUntil((route) => true); + _currentMenusNumber = 0; _printMenus(); } @@ -121,7 +135,7 @@ class NamidaNavigator { void onFirstLoad() { final initialTab = settings.selectedLibraryTab.value; final isSearchTab = initialTab == LibraryTab.search; - final finalTab = isSearchTab ? settings.libraryTabs.first : initialTab; + final finalTab = isSearchTab ? settings.libraryTabs.value.first : initialTab; navigateTo(finalTab.toWidget(), durationInMs: 0); Dimensions.inst.updateAllTileDimensions(); if (isSearchTab) ScrollSearchController.inst.animatePageController(initialTab); @@ -185,8 +199,8 @@ class NamidaNavigator { _isInFullScreen = true; WakelockController.inst.updateFullscreenStatus(true); - Get.to( - () => WillPopScope( + _rootNav.currentState?.pushPage( + WillPopScope( onWillPop: () async { if (onWillPop != null) await onWillPop(); exitFullScreen(); @@ -194,13 +208,9 @@ class NamidaNavigator { }, child: widget, ), - id: null, - preventDuplicates: true, transition: Transition.noTransition, - curve: Curves.easeOut, - duration: Duration.zero, - opaque: true, - fullscreenDialog: false, + durationInMs: 0, + maintainState: true, ); setDefaultSystemUIOverlayStyle(semiTransparent: true); @@ -212,7 +222,7 @@ class NamidaNavigator { Future exitFullScreen() async { if (!_isInFullScreen) return; - Get.until((route) => route.isFirst); + _rootNav.currentState?.pop(); setDefaultSystemUIOverlayStyle(); await Future.wait([ @@ -229,23 +239,37 @@ class NamidaNavigator { Transition transition = Transition.cupertino, int durationInMs = _defaultRouteAnimationDurMS, }) async { - currentWidgetStack.add(page.toNamidaRoute()); + final newRoute = page.toNamidaRoute(); + currentWidgetStack.add(newRoute); _hideEverything(); - currentRoute?.updateColorScheme(); + newRoute.updateColorScheme(); - await Get.to( - () => page, - id: 1, - preventDuplicates: false, + await navKey.currentState?.pushPage( + page, + durationInMs: durationInMs, transition: transition, - curve: Curves.easeOut, - duration: Duration(milliseconds: durationInMs), - opaque: true, - fullscreenDialog: false, + maintainState: true, ); } + Future navigateToRoot( + Widget page, { + Transition transition = Transition.cupertino, + int durationInMs = _defaultRouteAnimationDurMS, + }) async { + return await _rootNav.currentState?.pushPage( + page, + durationInMs: durationInMs, + transition: transition, + maintainState: true, + ); + } + + Future popRoot([T? result]) async { + return _rootNav.currentState?.pop(result); + } + /// Use [dialogBuilder] in case you want to acess the theme generated by [colorScheme]. Future navigateDialog({ final Widget? dialog, @@ -259,9 +283,6 @@ class NamidaNavigator { final bool blackBg = false, final void Function()? onDisposing, }) async { - final rootNav = navigator; - if (rootNav == null) return; - ScrollSearchController.inst.unfocusKeyboard(); _currentDialogNumber++; @@ -279,8 +300,8 @@ class NamidaNavigator { final theme = AppThemes.inst.getAppTheme(colorScheme, null, lighterDialogColor); - await Get.to( - () => WillPopScope( + await _rootNav.currentState?.pushPage( + WillPopScope( onWillPop: onWillPop, child: TapDetector( onTap: onWillPop, @@ -300,11 +321,11 @@ class NamidaNavigator { ), ), ), - duration: Duration(milliseconds: durationInMs), - preventDuplicates: false, + durationInMs: durationInMs, opaque: false, fullscreenDialog: true, transition: Transition.fade, + maintainState: true, ); if (onDisposing != null) { onDisposing.executeAfterDelay(durationMS: durationInMs * 2); @@ -314,9 +335,12 @@ class NamidaNavigator { Future closeDialog([int count = 1]) async { if (_currentDialogNumber == 0) return; - final closeCount = count.withMaximum(_currentDialogNumber); - _currentDialogNumber -= closeCount; - Get.close(closeCount); + int closeCount = count.withMaximum(_currentDialogNumber); + while (closeCount > 0) { + _currentDialogNumber--; + _rootNav.currentState?.pop(); + closeCount--; + } _printDialogs(); } @@ -333,21 +357,23 @@ class NamidaNavigator { Transition transition = Transition.cupertino, int durationInMs = _defaultRouteAnimationDurMS, }) async { - currentWidgetStack.removeLast(); - currentWidgetStack.add(page.toNamidaRoute()); + final newRoute = page.toNamidaRoute(); + currentWidgetStack.execute( + (value) { + value.removeLast(); + value.add(newRoute); + }, + ); + _hideEverything(); - currentRoute?.updateColorScheme(); + newRoute.updateColorScheme(); - await Get.off( - () => page, - id: 1, - preventDuplicates: false, + await navKey.currentState?.pushPageReplacement( + page, + durationInMs: durationInMs, transition: transition, - curve: Curves.easeOut, - duration: Duration(milliseconds: durationInMs), - opaque: true, - fullscreenDialog: false, + maintainState: true, ); } @@ -355,17 +381,19 @@ class NamidaNavigator { Widget page, { Transition transition = Transition.cupertino, }) async { + final newRoute = page.toNamidaRoute(); currentWidgetStack.value = [page.toNamidaRoute()]; _hideEverything(); - currentRoute?.updateColorScheme(); + newRoute.updateColorScheme(); + + navKey.currentState?.popUntil((r) => r.isFirst); - await Get.offAll( - () => page, - id: 1, + await navKey.currentState?.pushPageReplacement( + page, + durationInMs: 500, transition: transition, - curve: Curves.easeOut, - duration: const Duration(milliseconds: 500), + maintainState: true, ); } @@ -381,7 +409,7 @@ class NamidaNavigator { return; } if (isInYTCommentsSubpage) { - ytMiniplayerCommentsPageKey?.currentState?.pop(); + ytMiniplayerCommentsPageKey.currentState?.pop(); isInYTCommentsSubpage = false; return; } @@ -391,7 +419,10 @@ class NamidaNavigator { return; } - final ytsnvks = ytLocalSearchNavigatorKey?.currentState; + final miniplayerAllowPop = MiniPlayerController.inst.onWillPop(); + if (!miniplayerAllowPop) return; + + final ytsnvks = ytLocalSearchNavigatorKey.currentState; if (ytsnvks != null) { ytsnvks.pop(); isytLocalSearchInFullPage = false; @@ -402,15 +433,23 @@ class NamidaNavigator { _hideSearchMenuAndUnfocus(); return; } - if (SettingsSearchController.inst.canShowSearch) { + if (SettingsSearchController.inst.canShowSearch.value) { SettingsSearchController.inst.closeSearch(); return; } - if (currentRoute?.route == RouteType.PAGE_folders) { - final canIgoBackPls = Folders.inst.onBackButton(); - if (!canIgoBackPls) return; + final route = currentRoute?.route; + if (route != null) { + if (route == RouteType.PAGE_folders) { + final canIgoBackPls = Folders.inst.onBackButton(); + if (!canIgoBackPls) return; + } else if (route == RouteType.SUBPAGE_playlistTracks) { + PlaylistController.inst.resetCanReorder(); + } else if (route == RouteType.YOUTUBE_PLAYLIST_SUBPAGE) { + YoutubePlaylistController.inst.resetCanReorder(); + } } + if (_currentMenusNumber > 0) { return; } @@ -418,7 +457,7 @@ class NamidaNavigator { // pop only if not in root, otherwise show _doubleTapToExit(). if (currentWidgetStack.length > 1) { currentWidgetStack.removeLast(); - navKey?.currentState?.pop(); + navKey.currentState?.pop(); } else { await _doubleTapToExit(); } @@ -437,11 +476,11 @@ class NamidaNavigator { icon: Broken.logout, message: lang.EXIT_APP_SUBTITLE, top: false, - margin: const EdgeInsets.all(8.0), + margin: const EdgeInsets.all(12.0), animationDurationMS: 500, - snackbarStatus: (status) { + onStatusChanged: (status) { // -- resets time - if (status == SnackbarStatus.CLOSED) { + if (status == SnackbarStatus.closing || status == SnackbarStatus.closed) { _currentBackPressTime = DateTime(0); } }, @@ -456,67 +495,132 @@ class NamidaNavigator { void snackyy({ IconData? icon, - Widget? iconWidget, String title = '', required String message, bool top = true, - void Function(SnackbarStatus?)? snackbarStatus, - EdgeInsets margin = const EdgeInsets.symmetric(horizontal: 18.0, vertical: 4.0), + void Function(SnackbarStatus status)? onStatusChanged, + EdgeInsets margin = const EdgeInsets.symmetric(horizontal: 24.0, vertical: 8.0), int animationDurationMS = 600, int displaySeconds = 2, double borderRadius = 12.0, Color? leftBarIndicatorColor, - Widget? button, + (String text, FutureOr Function() function)? button, bool? isError, int? maxLinesMessage, }) { isError ??= title == lang.ERROR; - final view = Get.context == null ? null : View.of(Get.context!); - Get.showSnackbar( - GetSnackBar( - maxWidth: view == null ? null : view.physicalSize.shortestSide / view.devicePixelRatio, - alignment: Alignment.centerLeft, - icon: iconWidget ?? - (icon == null - ? null - : Center( - child: Icon(icon), - )), - titleText: title == '' - ? null - : Text( - title, - style: Get.textTheme.displayLarge, + final context = namida.context; + final view = context?.view ?? namida.platformView; + final backgroundColor = context?.theme.scaffoldBackgroundColor.withOpacity(0.3) ?? Colors.black54; + final itemsColor = context?.theme.colorScheme.onSurface.withOpacity(0.7) ?? Colors.white54; + + TextStyle getTextStyle(FontWeight fontWeight, double size, {bool action = false}) => TextStyle( + fontWeight: fontWeight, + fontSize: size, + color: action ? null : itemsColor, + fontFamily: "LexendDeca", + fontFamilyFallback: const ['sans-serif', 'Roboto'], + ); + + final borderR = borderRadius == 0 ? null : BorderRadius.circular(borderRadius.multipliedRadius); + SnackbarController? snackbarController; + + final EdgeInsets paddingInsets; + if (button != null) { + paddingInsets = const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0); + } else if (icon != null || title != '') { + paddingInsets = const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0); + } else { + paddingInsets = const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0); + } + final content = Padding( + padding: paddingInsets, + child: SizedBox( + width: view == null ? null : view.physicalSize.shortestSide / view.devicePixelRatio, + child: Row( + children: [ + if (icon != null) + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Icon(icon, color: itemsColor), + ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (title != '') + Text( + title, + style: getTextStyle(FontWeight.w700, 17.0), + ), + Text( + message, + style: title != '' ? getTextStyle(FontWeight.w400, 14.0) : getTextStyle(FontWeight.w600, 15.0), + maxLines: maxLinesMessage, + overflow: maxLinesMessage == null ? null : TextOverflow.ellipsis, + ), + ], ), - messageText: Text( - message, - style: title != '' ? Get.textTheme.displaySmall : Get.textTheme.displayMedium, - maxLines: maxLinesMessage, - overflow: TextOverflow.ellipsis, + ), + if (button != null) + TextButton( + style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), + onPressed: () { + button.$2(); + snackbarController?.close(); + }, + child: NamidaButtonText( + button.$1, + style: getTextStyle(FontWeight.bold, 14.0, action: true), + ), + ), + ], + ), + ), + ); + final snackbar = NamSnackBar( + margin: margin, + duration: Duration(seconds: displaySeconds), + animationDuration: Duration(milliseconds: animationDurationMS), + alignment: Alignment.centerLeft, + top: top, + forwardAnimationCurve: Curves.fastLinearToSlowEaseIn, + reverseAnimationCurve: Curves.easeInOutQuart, + onStatusChanged: onStatusChanged, + child: Material( + color: Colors.transparent, + type: MaterialType.transparency, + child: ClipRRect( + borderRadius: borderR ?? BorderRadius.zero, + child: NamidaBgBlur( + blur: 12.0, + child: DecoratedBox( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: borderR, + border: isError ? Border.all(color: Colors.red.withOpacity(0.2), width: 1.5) : Border.all(color: Colors.grey.withOpacity(0.5), width: 0.5), + boxShadow: isError + ? [ + BoxShadow( + color: Colors.red.withAlpha(15), + blurRadius: 16.0, + ) + ] + : null, + ), + child: leftBarIndicatorColor != null + ? DecoratedBox( + decoration: BoxDecoration(border: Border(left: BorderSide(color: leftBarIndicatorColor, width: 4.5))), + child: content, + ) + : content, + ), + ), ), - mainButton: button, - margin: margin, - snackPosition: top ? SnackPosition.TOP : SnackPosition.BOTTOM, - leftBarIndicatorColor: leftBarIndicatorColor, - borderWidth: 1.5, - borderColor: isError ? Colors.red.withOpacity(0.2) : null, - boxShadows: isError - ? [ - BoxShadow( - color: Colors.red.withAlpha(15), - blurRadius: 16.0, - ) - ] - : null, - shouldIconPulse: false, - backgroundColor: Get.theme.scaffoldBackgroundColor.withOpacity(0.3), - borderRadius: borderRadius.multipliedRadius, - barBlur: 12.0, - animationDuration: Duration(milliseconds: animationDurationMS), - forwardAnimationCurve: Curves.fastLinearToSlowEaseIn, - reverseAnimationCurve: Curves.easeInOutQuart, - duration: Duration(seconds: displaySeconds), - snackbarStatus: snackbarStatus, ), ); + + snackbarController = SnackbarController(snackbar); + snackbarController.show(); } diff --git a/lib/controller/player_controller.dart b/lib/controller/player_controller.dart index bbd280d1..a69557e1 100644 --- a/lib/controller/player_controller.dart +++ b/lib/controller/player_controller.dart @@ -1,9 +1,11 @@ +// ignore_for_file: avoid_rx_value_getter_outside_obx import 'dart:async'; import 'dart:io'; import 'package:audio_service/audio_service.dart'; -import 'package:get/get.dart'; +import 'package:basic_audio_handler/basic_audio_handler.dart'; import 'package:just_audio/just_audio.dart'; +import 'package:namida/core/utils.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; import 'package:namida/class/audio_cache_detail.dart'; @@ -35,80 +37,106 @@ class Player { Map> get audioCacheMap => _audioHandler.audioCacheMap; - Track get nowPlayingTrack => _audioHandler.currentTrack.track; - Selectable get nowPlayingTWD => _audioHandler.currentTrack; - List get currentQueue => _audioHandler.currentQueueSelectable; + Selectable? get currentTrack { + final item = _audioHandler.currentItem.value; + return item is Selectable ? item : null; + } + + Selectable? get currentTrackR { + final item = _audioHandler.currentItem.valueR; + return item is Selectable ? item : null; + } + + YoutubeID? get currentVideo { + final item = _audioHandler.currentItem.value; + return item is YoutubeID ? item : null; + } + + YoutubeID? get currentVideoR { + final item = _audioHandler.currentItem.valueR; + return item is YoutubeID ? item : null; + } - String get getCurrentVideoId => (YoutubeController.inst.currentYoutubeMetadataVideo.value ?? currentVideoInfo)?.id ?? nowPlayingVideoID?.id ?? nowPlayingTrack.youtubeID; + RxBaseCore> get currentQueue => _audioHandler.currentQueue; + Rx get currentItem => _audioHandler.currentItem; - YoutubeID? get nowPlayingVideoID => _audioHandler.currentVideo; - List get currentQueueYoutube => _audioHandler.currentQueueYoutubeID; + String get getCurrentVideoIdR => (YoutubeController.inst.currentYoutubeMetadataVideo.valueR ?? currentVideoInfo.valueR)?.id ?? currentVideoR?.id ?? ''; + String get getCurrentVideoId => (YoutubeController.inst.currentYoutubeMetadataVideo.value ?? currentVideoInfo.value)?.id ?? currentVideo?.id ?? ''; - VideoInfoData? get videoPlayerInfo => _audioHandler.videoPlayerInfo.value; - bool get videoInitialized => videoPlayerInfo?.isInitialized ?? false; + RxBaseCore get videoPlayerInfo => _audioHandler.videoPlayerInfo; AndroidEqualizer get equalizer => _audioHandler.equalizer; AndroidLoudnessEnhancer get loudnessEnhancer => _audioHandler.loudnessEnhancer; int? get androidSessionId => _audioHandler.androidSessionId; - VideoInfo? get currentVideoInfo => _audioHandler.currentVideoInfo.value; - YoutubeChannel? get currentChannelInfo => _audioHandler.currentChannelInfo.value; - VideoOnlyStream? get currentVideoStream => _audioHandler.currentVideoStream.value; - AudioOnlyStream? get currentAudioStream => _audioHandler.currentAudioStream.value; - File? get currentVideoThumbnail => _audioHandler.currentVideoThumbnail.value; - NamidaVideo? get currentCachedVideo => _audioHandler.currentCachedVideo.value; - AudioCacheDetails? get currentCachedAudio => _audioHandler.currentCachedAudio.value; + RxBaseCore get currentVideoInfo => _audioHandler.currentVideoInfo; + RxBaseCore get currentChannelInfo => _audioHandler.currentChannelInfo; + RxBaseCore get currentVideoStream => _audioHandler.currentVideoStream; + RxBaseCore get currentAudioStream => _audioHandler.currentAudioStream; + RxBaseCore get currentCachedVideo => _audioHandler.currentCachedVideo; + RxBaseCore get currentCachedAudio => _audioHandler.currentCachedAudio; + + Duration get getCurrentVideoDurationR { + Duration? playerDuration = currentItemDuration.valueR; + if (playerDuration == null || playerDuration == Duration.zero) { + playerDuration = currentAudioStream.valueR?.durationMS?.milliseconds ?? + currentVideoStream.valueR?.durationMS?.milliseconds ?? + (currentVideo == null ? VideoController.inst.currentVideo.valueR?.durationMS.milliseconds : YoutubeController.inst.currentYoutubeMetadataVideo.valueR?.duration) ?? + Duration.zero; + } + return playerDuration; + } Duration get getCurrentVideoDuration { - Duration? playerDuration = Player.inst.currentItemDuration; + Duration? playerDuration = Player.inst.currentItemDuration.value; if (playerDuration == null || playerDuration == Duration.zero) { - playerDuration = Player.inst.currentAudioStream?.durationMS?.milliseconds ?? - Player.inst.currentVideoStream?.durationMS?.milliseconds ?? - (nowPlayingVideoID == null ? VideoController.inst.currentVideo.value?.durationMS.milliseconds : YoutubeController.inst.currentYoutubeMetadataVideo.value?.duration) ?? + playerDuration = currentAudioStream.value?.durationMS?.milliseconds ?? + currentVideoStream.value?.durationMS?.milliseconds ?? + (currentVideo == null ? VideoController.inst.currentVideo.value?.durationMS.milliseconds : YoutubeController.inst.currentYoutubeMetadataVideo.value?.duration) ?? Duration.zero; } return playerDuration; } - bool get isAudioOnlyPlayback => _audioHandler.isAudioOnlyPlayback; bool get isCurrentAudioFromCache => _audioHandler.isCurrentAudioFromCache; - Stream get positionStream => _audioHandler.positionStream; - int get currentIndex => _audioHandler.currentIndex; - int get nowPlayingPosition => _audioHandler.currentPositionMS; - double get currentSpeed => _audioHandler.currentSpeed; - Duration? get currentItemDuration => _audioHandler.currentItemDuration; - bool get isPlaying => _audioHandler.isPlaying; - bool get isBuffering => _audioHandler.currentState == ProcessingState.buffering; - bool get isLoading => _audioHandler.currentState == ProcessingState.loading; - bool get isFetchingInfo => _audioHandler.isFetchingInfo; - bool get shouldShowLoadingIndicator { - if (isBuffering || isLoading) return true; - if (isFetchingInfo && _audioHandler.currentState != ProcessingState.ready) return true; + RxBaseCore get currentIndex => _audioHandler.currentIndex; + RxBaseCore get nowPlayingPosition => _audioHandler.currentPositionMS; + int get nowPlayingPositionR => _audioHandler.currentPositionMS.valueR; + RxBaseCore get currentSpeed => _audioHandler.currentSpeed; + RxBaseCore get currentItemDuration => _audioHandler.currentItemDuration; + RxBaseCore get isPlaying => _audioHandler.isPlaying; + bool get isPlayingR => _audioHandler.isPlaying.valueR; + bool get isBufferingR => _audioHandler.currentState.valueR == ProcessingState.buffering; + bool get isLoadingR => _audioHandler.currentState.valueR == ProcessingState.loading; + RxBaseCore get isFetchingInfo => _audioHandler.isFetchingInfo; + bool get shouldShowLoadingIndicatorR { + if (isBufferingR || isLoadingR) return true; + if (isFetchingInfo.valueR && _audioHandler.currentState.valueR != ProcessingState.ready) return true; return false; } - Duration get buffered => _audioHandler.buffered; - int get numberOfRepeats => _audioHandler.numberOfRepeats; + RxBaseCore get buffered => _audioHandler.buffered; + RxBaseCore get numberOfRepeats => _audioHandler.numberOfRepeats; int get latestInsertedIndex => _audioHandler.latestInsertedIndex; - bool get enableSleepAfterTracks => _audioHandler.enableSleepAfterItems; - bool get enableSleepAfterMins => _audioHandler.enableSleepAfterMins; - int get sleepAfterMin => _audioHandler.sleepAfterMin; - int get sleepAfterTracks => _audioHandler.sleepAfterItems; - bool get isLastItem => _audioHandler.isLastItem; - bool get canJumpToNext => !isLastItem || settings.player.infiniyQueueOnNextPrevious.value; - bool get canJumpToPrevious => currentIndex != 0 || settings.player.infiniyQueueOnNextPrevious.value; + RxBaseCore get sleepTimerConfig => _audioHandler.sleepTimerConfig; + + bool get canJumpToNext => !_audioHandler.isLastItem || settings.player.infiniyQueueOnNextPrevious.value; + bool get canJumpToPrevious => currentIndex.value != 0 || settings.player.infiniyQueueOnNextPrevious.value; RxMap? get totalListenedTimeInSec => _audioHandler.totalListenedTimeInSec; - int get sleepingTrackIndex => sleepAfterTracks + currentIndex - 1; + int sleepingItemIndex(int sleepAfterItems, int currentIndex) => sleepAfterItems + currentIndex - 1; + + void setPositionListener(void Function(int ms)? fn) { + _audioHandler.positionListener = fn; + } // -- error playing track void cancelPlayErrorSkipTimer() => _audioHandler.cancelPlayErrorSkipTimer(); - int get playErrorRemainingSecondsToSkip => _audioHandler.playErrorRemainingSecondsToSkip; + RxBaseCore get playErrorRemainingSecondsToSkip => _audioHandler.playErrorRemainingSecondsToSkip; - StreamSubscription? _videoPlayerInfoSub; StreamSubscription? _notificationClickedSub; Future initializePlayer() async { @@ -127,15 +155,20 @@ class Player { return imagePath != null ? File(imagePath).statSync().toString() : ''; }, ); - _videoPlayerInfoSub?.cancel(); - _videoPlayerInfoSub = _audioHandler.videoPlayerInfo.listen((info) { + + void videoInfoListener() { + final info = _audioHandler.videoPlayerInfo.value; if (info == null || info.width == -1 || info.height == -1) { WakelockController.inst.updateVideoStatus(false); } else { WakelockController.inst.updateVideoStatus(true); NamidaChannel.inst.updatePipRatio(width: info.width, height: info.height); } - }); + } + + _audioHandler.videoPlayerInfo.removeListener(videoInfoListener); + _audioHandler.videoPlayerInfo.addListener(videoInfoListener); + prepareTotalListenTime(); setSkipSilenceEnabled(settings.player.skipSilenceEnabled.value); AudioService.setLockScreenArtwork(settings.player.lockscreenArtwork.value); @@ -167,7 +200,7 @@ class Player { } else { if (eq.equalizer.isNotEmpty) { _audioHandler.equalizer.parameters.then((parameters) { - parameters.bands.loop((b, index) { + parameters.bands.loop((b) { final userGain = eq.equalizer[b.centerFrequency]; if (userGain != null) b.setGain(userGain); }); @@ -223,21 +256,23 @@ class Player { } void updateSleepTimerValues({ - bool? enableSleepAfterTracks, + bool? enableSleepAfterItems, bool? enableSleepAfterMins, int? sleepAfterMin, - int? sleepAfterTracks, + int? sleepAfterItems, }) { - _audioHandler.updateSleepTimerValues( - enableSleepAfterItems: enableSleepAfterTracks, - enableSleepAfterMins: enableSleepAfterMins, - sleepAfterMin: sleepAfterMin, - sleepAfterItems: sleepAfterTracks, + final oldConfig = sleepTimerConfig.value; + final newConfig = SleepTimerConfig( + enableSleepAfterItems: enableSleepAfterItems ?? oldConfig.enableSleepAfterItems, + enableSleepAfterMins: enableSleepAfterMins ?? oldConfig.enableSleepAfterMins, + sleepAfterMin: sleepAfterMin ?? oldConfig.sleepAfterMin, + sleepAfterItems: sleepAfterItems ?? oldConfig.sleepAfterItems, ); + _audioHandler.sleepTimerConfig.value = newConfig; } void resetSleepAfterTimer() { - _audioHandler.resetSleepAfterTimer(); + _audioHandler.resetSleepTimer(); } Future setVolume(double volume) async { @@ -250,7 +285,7 @@ class Player { FutureOr shuffleTracks(bool allTracks) async { if (allTracks) { - if (currentQueue.isNotEmpty) { + if (currentItem is Selectable) { _audioHandler.shuffleAllItems((element) => (element as Selectable).track); } else { _audioHandler.shuffleAllItems((element) => (element as YoutubeID).id); @@ -262,7 +297,7 @@ class Player { } int removeDuplicatesFromQueue() { - if (currentQueue.isNotEmpty) { + if (currentItem is Selectable) { return _audioHandler.removeDuplicatesFromQueue((element) => (element as Selectable).track); } else { return _audioHandler.removeDuplicatesFromQueue((element) => (element as YoutubeID).id); @@ -298,7 +333,7 @@ class Player { final addins = shouldInsertNext ? lang.INSERTED : lang.ADDED; snackyy( icon: shouldInsertNext ? Broken.redo : Broken.add_circle, - message: '${addins.capitalizeFirst} ${finalTracks.displayTrackKeyword}', + message: '${addins.capitalizeFirst()} ${finalTracks.displayTrackKeyword}', ); } return true; @@ -319,7 +354,7 @@ class Player { final addins = shouldInsertNext ? lang.INSERTED : lang.ADDED; snackyy( icon: shouldInsertNext ? Broken.redo : Broken.add_circle, - message: '${addins.capitalizeFirst} ${finalVideos.length.displayVideoKeyword}', + message: '${addins.capitalizeFirst()} ${finalVideos.length.displayVideoKeyword}', top: false, ); } @@ -335,7 +370,7 @@ class Player { Future removeFromQueue(int index) async { // why [isPlaying] ? imagine removing while paused - await _audioHandler.removeFromQueue(index, isPlaying && _audioHandler.defaultShouldStartPlayingWhenPaused); + await _audioHandler.removeFromQueue(index, isPlaying.value && _audioHandler.defaultShouldStartPlayingWhenPaused); } Future replaceAllTracksInQueue(Playable oldTrack, Playable newTrack) async { @@ -348,7 +383,7 @@ class Player { Future replaceTracksDirectoryInQueue(String oldDir, String newDir, {Iterable? forThesePathsOnly, bool ensureNewFileExists = false}) async { String getNewPath(String old) => old.replaceFirst(oldDir, newDir); - if (currentQueue.isNotEmpty) { + if (currentItem is Selectable) { await _audioHandler.replaceWhereInQueue( (e) { final trackPath = (e as Selectable).track.path; @@ -457,21 +492,21 @@ class Player { Future seekSecondsForward({int? seconds, void Function(int finalSeconds)? onSecondsReady}) async { final newSeconds = _secondsToSeek(seconds); onSecondsReady?.call(newSeconds); - await _audioHandler.seek(Duration(milliseconds: nowPlayingPosition + newSeconds * 1000)); + await _audioHandler.seek(Duration(milliseconds: nowPlayingPosition.value + newSeconds * 1000)); } /// Default value is set to user preference [seekDurationInSeconds] Future seekSecondsBackward({int? seconds, void Function(int finalSeconds)? onSecondsReady}) async { final newSeconds = _secondsToSeek(seconds); onSecondsReady?.call(newSeconds); - await _audioHandler.seek(Duration(milliseconds: nowPlayingPosition - newSeconds * 1000)); + await _audioHandler.seek(Duration(milliseconds: nowPlayingPosition.value - newSeconds * 1000)); } int _secondsToSeek([int? seconds]) { int? newSeconds = seconds; if (newSeconds == null) { if (settings.player.isSeekDurationPercentage.value) { - final sFromP = (currentItemDuration?.inSeconds ?? 0) * (settings.player.seekDurationInPercentage.value / 100); + final sFromP = (currentItemDuration.value?.inSeconds ?? 0) * (settings.player.seekDurationInPercentage.value / 100); newSeconds = sFromP.toInt(); } else { newSeconds = settings.player.seekDurationInSeconds.value; @@ -525,3 +560,10 @@ class Player { await _audioHandler.setVideo(null); } } + +extension QueueListExt on List { + Iterable mapAs() { + if (Player._instance.currentItem is! T) return []; + return this.map((e) => e as T); + } +} diff --git a/lib/controller/playlist_controller.dart b/lib/controller/playlist_controller.dart index e4c537a2..d79ee483 100644 --- a/lib/controller/playlist_controller.dart +++ b/lib/controller/playlist_controller.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:path/path.dart' as p; import 'package:playlist_manager/playlist_manager.dart'; @@ -21,6 +20,7 @@ import 'package:namida/core/functions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; typedef Playlist = GeneralPlaylist; @@ -30,7 +30,8 @@ class PlaylistController extends PlaylistManager { static final PlaylistController _instance = PlaylistController._internal(); PlaylistController._internal(); - final RxBool canReorderTracks = false.obs; + final canReorderTracks = false.obs; + void resetCanReorder() => canReorderTracks.value = false; void addNewPlaylist( String name, { @@ -82,10 +83,10 @@ class PlaylistController extends PlaylistManager { break; case PlaylistAddDuplicateAction.addAllAndRemoveOldOnes: final currentTracks = >{}; - playlist.tracks.loop((e, index) => currentTracks.addForce(e.track, index)); + playlist.tracks.loopAdv((e, index) => currentTracks.addForce(e.track, index)); final indicesToRemove = []; - tracks.loop((e, _) { + tracks.loop((e) { // -- removing same tracks existing in playlist final indexesInPlaylist = currentTracks[e]; if (indexesInPlaylist != null) { @@ -93,13 +94,13 @@ class PlaylistController extends PlaylistManager { } }); indicesToRemove.sortByReverse((e) => e); - indicesToRemove.loop((indexToRemove, _) => playlist.tracks.removeAt(indexToRemove)); + indicesToRemove.loop((indexToRemove) => playlist.tracks.removeAt(indexToRemove)); playlist.tracks.addAll(convertTracks(tracks)); break; case PlaylistAddDuplicateAction.addOnlyMissing: final currentTracks = {}; - playlist.tracks.loop((e, index) => currentTracks[e.track] = index); - tracks.loop((e, _) { + playlist.tracks.loopAdv((e, index) => currentTracks[e.track] = index); + tracks.loop((e) { if (currentTracks[e] == null) { playlist.tracks.add(convertTrack(e)); } else { @@ -119,13 +120,12 @@ class PlaylistController extends PlaylistManager { snackyy( message: "${lang.ADDED} ${addedTracksLength.displayTrackKeyword}", displaySeconds: 2, - button: TextButton( - onPressed: () async { - updatePropertyInPlaylist(playlist.name, tracks: oldTracksList, modifiedDate: currentTimeMS); - Get.closeAllSnackbars(); - }, - child: Text(lang.UNDO), - ), + button: addedTracksLength > 0 + ? ( + lang.UNDO, + () async => await updatePropertyInPlaylist(playlist.name, tracks: oldTracksList, modifiedDate: currentTimeMS), + ) + : null, ); super.addTracksToPlaylistRaw(playlist, [] /* added manually */); @@ -146,11 +146,12 @@ class PlaylistController extends PlaylistManager { action.value = null; NamidaNavigator.inst.closeDialog(); }, - child: Text(lang.CANCEL), + child: NamidaButtonText(lang.CANCEL), ), - Obx( - () => NamidaButton( - enabled: action.value != null, + ObxO( + rx: action, + builder: (action) => NamidaButton( + enabled: action != null, text: lang.CONFIRM, onPressed: NamidaNavigator.inst.closeDialog, ), @@ -163,7 +164,7 @@ class PlaylistController extends PlaylistManager { children: [ Text( lang.DUPLICATED_ITEMS_ADDING, - style: Get.textTheme.displayMedium, + style: namida.textTheme.displayMedium, ), const SizedBox(height: 12.0), Column( @@ -171,9 +172,10 @@ class PlaylistController extends PlaylistManager { .map( (e) => Padding( padding: const EdgeInsets.all(3.0), - child: Obx( - () => ListTileWithCheckMark( - active: action.value == e, + child: ObxO( + rx: action, + builder: (act) => ListTileWithCheckMark( + active: act == e, title: e.toText(), onTap: () => action.value = e, ), @@ -246,6 +248,13 @@ class PlaylistController extends PlaylistManager { await replaceTheseTracksInPlaylistsBulk(fnList); } + @override + Future renamePlaylist(String playlistName, String newName) async { + final didRename = await super.renamePlaylist(playlistName, newName); + if (didRename) _popPageIfCurrent(() => playlistName); + return didRename; + } + /// Returns number of generated tracks. int generateRandomPlaylist() { final rt = NamidaGenerator.inst.getRandomTracks(); @@ -426,9 +435,10 @@ class PlaylistController extends PlaylistManager { actions: [ const CancelButton(), const SizedBox(width: 8.0), - Obx( - () => NamidaButton( - enabled: didRead.value, + ObxO( + rx: didRead, + builder: (didRead) => NamidaButton( + enabled: didRead, text: lang.CONFIRM, onPressed: () { settings.save(enableM3USync: true); @@ -442,16 +452,14 @@ class PlaylistController extends PlaylistManager { children: [ Text( '${lang.ENABLE_M3U_SYNC}?\n\n${lang.ENABLE_M3U_SYNC_NOTE_1}\n\n${lang.ENABLE_M3U_SYNC_NOTE_2.replaceFirst('_PLAYLISTS_BACKUP_PATH_', AppDirs.M3UBackup)}\n\n${lang.WARNING.toUpperCase()}: ${lang.ENABLE_M3U_SYNC_SUBTITLE}', - style: Get.textTheme.displayMedium, + style: namida.textTheme.displayMedium, ), const SizedBox(height: 12.0), - Obx( - () => ListTileWithCheckMark( - icon: Broken.info_circle, - active: didRead.value, - title: lang.I_READ_AND_AGREE, - onTap: () => didRead.value = !didRead.value, - ), + ListTileWithCheckMark( + activeRx: didRead, + icon: Broken.info_circle, + title: lang.I_READ_AND_AGREE, + onTap: didRead.toggle, ), ], ), @@ -525,14 +533,18 @@ class PlaylistController extends PlaylistManager { @override FutureOr canRemovePlaylist(GeneralPlaylist playlist) { - // navigate back in case the current route is this playlist + _popPageIfCurrent(() => playlist.name); + return true; + } + + /// Navigate back in case the current route is this playlist. + void _popPageIfCurrent(String Function() playlistName) { final lastPage = NamidaNavigator.inst.currentRoute; if (lastPage?.route == RouteType.SUBPAGE_playlistTracks) { - if (lastPage?.name == playlist.name) { + if (lastPage?.name == playlistName()) { NamidaNavigator.inst.popPage(); } } - return true; } @override diff --git a/lib/controller/queue_controller.dart b/lib/controller/queue_controller.dart index 6ac809c1..1d50199c 100644 --- a/lib/controller/queue_controller.dart +++ b/lib/controller/queue_controller.dart @@ -1,7 +1,7 @@ import 'dart:collection'; import 'dart:io'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/queue.dart'; import 'package:namida/class/track.dart'; @@ -71,7 +71,7 @@ class QueueController { Future removeQueues(List queuesDates) async { bool hasLatestAdded = false; - queuesDates.loop((date, _) { + queuesDates.loop((date) { queuesMap.value.remove(date); if (date == _latestAddedQueueDate) hasLatestAdded = true; }); @@ -141,7 +141,7 @@ class QueueController { String getNewPath(String old) => old.replaceFirst(oldDir, newDir); final queuesToSave = {}; - queuesMap.value.entries.toList().loop((entry, index) { + queuesMap.value.entries.toList().loop((entry) { final q = entry.value; q.tracks.replaceWhere( (e) { @@ -165,7 +165,7 @@ class QueueController { Future replaceTrackInAllQueues(Map oldNewTrack) async { final queuesToSave = []; - queuesMap.value.entries.toList().loop((entry, index) { + queuesMap.value.entries.toList().loop((entry) { final q = entry.value; for (final e in oldNewTrack.entries) { q.tracks.replaceItems( @@ -233,10 +233,10 @@ class QueueController { index = settings.player.lastPlayedIndices[t] ?? 0; switch (t) { case LibraryCategory.localTracks: - items.loop((e, _) => latestQueue.add(Track(e))); + items.loop((e) => latestQueue.add(Track(e))); break; case LibraryCategory.youtube: - items.loop((e, _) => latestQueue.add(YoutubeID.fromJson(e))); + items.loop((e) => latestQueue.add(YoutubeID.fromJson(e))); break; // case LibraryCategory.localVideos: // break; @@ -263,17 +263,17 @@ class QueueController { String type = ''; final queue = []; switch (items.firstOrNull.runtimeType) { - case Track: + case const (Track): type = LibraryCategory.localTracks; - (items.cast()).loop((e, _) => queue.add(e.path)); + (items.cast()).loop((e) => queue.add(e.path)); break; - case TrackWithDate: + case const (TrackWithDate): type = LibraryCategory.localTracks; - (items.cast()).loop((e, _) => queue.add(e.track.path)); + (items.cast()).loop((e) => queue.add(e.track.path)); break; - case YoutubeID: + case const (YoutubeID): type = LibraryCategory.youtube; - (items.cast()).loop((e, _) => queue.add(e.toJson())); + (items.cast()).loop((e) => queue.add(e.toJson())); break; } final map = { @@ -292,7 +292,7 @@ class QueueController { } static void _deleteQueuesFromStorageIsolate((String, List) pathAndDates) async { - pathAndDates.$2.loop((date, _) { + pathAndDates.$2.loop((date) { try { File('${pathAndDates.$1}$date.json').deleteSync(); } catch (_) {} diff --git a/lib/controller/scroll_search_controller.dart b/lib/controller/scroll_search_controller.dart index 4ee0f437..7dadc69c 100644 --- a/lib/controller/scroll_search_controller.dart +++ b/lib/controller/scroll_search_controller.dart @@ -3,7 +3,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/controller/miniplayer_controller.dart'; import 'package:namida/controller/navigator_controller.dart'; @@ -24,11 +24,11 @@ class ScrollSearchController { final ytSearchKey = GlobalKey(); final currentSearchType = SearchType.localTracks.obs; - final RxBool isGlobalSearchMenuShown = false.obs; + final isGlobalSearchMenuShown = false.obs; final TextEditingController searchTextEditingController = TextEditingController(); - final Map isSearchBoxVisibleMap = {}; - final Map isBarVisibleMap = {}; + final Map> isSearchBoxVisibleMap = >{}; + final Map> isBarVisibleMap = >{}; ScrollController scrollController = ScrollController(); final Map scrollPositionsMap = {}; @@ -98,20 +98,20 @@ class ScrollSearchController { }); } - bool getIsSearchBoxVisible(LibraryTab tab) { + RxBaseCore getIsSearchBoxVisible(LibraryTab tab) { if (isSearchBoxVisibleMap[tab] != null) { - return isSearchBoxVisibleMap[tab]!.value; + return isSearchBoxVisibleMap[tab]!; } isSearchBoxVisibleMap[tab] = false.obs; - return isSearchBoxVisibleMap[tab]!.value; + return isSearchBoxVisibleMap[tab]!; } - bool getIsBarVisible(LibraryTab tab) { + RxBaseCore getIsBarVisible(LibraryTab tab) { if (isBarVisibleMap[tab] != null) { - return isBarVisibleMap[tab]!.value; + return isBarVisibleMap[tab]!; } isBarVisibleMap[tab] = true.obs; - return isBarVisibleMap[tab]!.value; + return isBarVisibleMap[tab]!; } double getScrollPosition(LibraryTab tab) { @@ -197,8 +197,8 @@ extension LibraryTabStuff on LibraryTab { ScrollController get scrollController => ScrollSearchController.inst.scrollController; TextEditingController? get textSearchController => ScrollSearchController.inst.textSearchControllers[this]; double get scrollPosition => ScrollSearchController.inst.getScrollPosition(this); - bool get isBarVisible => ScrollSearchController.inst.getIsBarVisible(this); - bool get isSearchBoxVisible => ScrollSearchController.inst.getIsSearchBoxVisible(this); + RxBaseCore get isBarVisible => ScrollSearchController.inst.getIsBarVisible(this); + RxBaseCore get isSearchBoxVisible => ScrollSearchController.inst.getIsSearchBoxVisible(this); double get offsetOrZero => (ScrollSearchController.inst.scrollController.hasClients) ? scrollController.positions.lastOrNull?.pixels ?? 0.0 : 0.0; bool get shouldAnimateTiles => offsetOrZero == 0.0; } diff --git a/lib/controller/search_ports_provider.dart b/lib/controller/search_ports_provider.dart index bc13e6c5..15e7c2c1 100644 --- a/lib/controller/search_ports_provider.dart +++ b/lib/controller/search_ports_provider.dart @@ -3,6 +3,7 @@ import 'dart:isolate'; import 'package:namida/base/ports_provider.dart'; import 'package:namida/core/enums.dart'; +import 'package:namida/core/extensions.dart'; class SearchPortsProvider with PortsProviderBase { static final SearchPortsProvider inst = SearchPortsProvider._internal(); @@ -11,10 +12,9 @@ class SearchPortsProvider with PortsProviderBase { final _ports = {}; void disposeAll() { - for (final p in _ports.values) { - if (p != null) disposePort(p); - } + final ports = _ports.values.whereType().toList(); _ports.clear(); + ports.loop(disposePort); } Future closePorts(MediaType type) async { diff --git a/lib/controller/search_sort_controller.dart b/lib/controller/search_sort_controller.dart index 26f79311..2dded2f8 100644 --- a/lib/controller/search_sort_controller.dart +++ b/lib/controller/search_sort_controller.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:isolate'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:intl/intl.dart'; import 'package:playlist_manager/playlist_manager.dart'; @@ -140,9 +140,9 @@ class SearchSortController { }; List getMediaTracksSortingComparables(MediaType media) { - final sorts = settings.mediaItemsTrackSorting[media] ?? [SortType.title]; + final sorts = settings.mediaItemsTrackSorting.value[media] ?? [SortType.title]; final l = []; - sorts.loop((e, index) { + sorts.loop((e) { if (_mediaTracksSortingComparables[e] != null) l.add(_mediaTracksSortingComparables[e]!); }); return l; @@ -154,7 +154,7 @@ class SearchSortController { _preparedResources = true; final enabledSearchesList = settings.activeSearchMediaTypes; final enabledSearches = {}; - enabledSearchesList.loop((f, _) => enabledSearches[f] = true); + enabledSearchesList.loop((f) => enabledSearches[f] = true); final mainMapArtists = Indexer.inst.mainMapArtists.value.keys; final mainMapAA = Indexer.inst.mainMapAlbumArtists.value.keys; @@ -206,7 +206,7 @@ class SearchSortController { Map generateTrackSearchIsolateParams(SendPort sendPort, {bool sendPrepared = false}) { final params = { - 'tracks': Indexer.inst.allTracksMappedByPath.values + 'tracks': Indexer.inst.allTracksMappedByPath.value.values .map((e) => { 'title': e.title, 'artist': e.originalArtist, @@ -247,14 +247,14 @@ class SearchSortController { }, isolateFunction: (itemsSendPort) async { final params = { - 'playlists': playlistsMap.values.map((e) => e.toJson((item) => item.toJson())).toList(), + 'playlists': playlistsMap.value.values.map((e) => e.toJson((item) => item.toJson())).toList(), 'translations': { 'k_PLAYLIST_NAME_AUTO_GENERATED': lang.AUTO_GENERATED, 'k_PLAYLIST_NAME_FAV': lang.FAVOURITES, 'k_PLAYLIST_NAME_HISTORY': lang.HISTORY, 'k_PLAYLIST_NAME_MOST_PLAYED': lang.MOST_PLAYED, }, - 'filters': settings.playlistSearchFilter.cast(), + 'filters': settings.playlistSearchFilter.value, 'cleanup': _shouldCleanup, 'sendPort': itemsSendPort, }; @@ -298,9 +298,7 @@ class SearchSortController { trackSearchTemp.clear(); } else { LibraryTab.tracks.textSearchController?.clear(); - trackSearchList - ..clear() - ..addAll(tracksInfoList); + trackSearchList.assignAll(tracksInfoList.value); } return; } @@ -326,7 +324,7 @@ class SearchSortController { sendPort.send(receivePort.sendPort); final tsfMap = {}; - tsf.loop((f, _) => tsfMap[f] = true); + tsf.loop((f) => tsfMap[f] = true); final stitle = tsfMap[TrackSearchFilter.title] ?? true; final sfilename = tsfMap[TrackSearchFilter.filename] ?? true; @@ -406,7 +404,7 @@ class SearchSortController { } final result = []; - tracksExtended.loop((trExt, index) { + tracksExtended.loop((trExt) { if ((stitle && isMatch(trExt.splitTitle)) || (sfilename && isMatch(trExt.splitFilename)) || (salbum && isMatch(trExt.splitAlbum)) || @@ -524,7 +522,7 @@ class SearchSortController { final textCleanedForSearch = _functionOfCleanup(cleanup); final psfMap = {}; - psf.loop((f, _) => psfMap[f] = true); + psf.loop((f) => psfMap[f] = true); final sTitle = psfMap['name'] ?? true; final sCreationDate = psfMap['creationDate'] ?? false; @@ -546,7 +544,7 @@ class SearchSortController { final lctext = textCleanedForSearch(text); final results = []; - playlists.loop((itemInfo, _) { + playlists.loop((itemInfo) { final item = itemInfo.pl; final playlistName = item.name; @@ -600,7 +598,7 @@ class SearchSortController { _sortTracksRaw( sortBy: sortBy ?? settings.tracksSort.value, reverse: reverse ?? settings.tracksSortReversed.value, - list: tracksInfoList, + list: tracksInfoList.value, onDone: (sortType, isReverse) { settings.save(tracksSort: sortType, tracksSortReversed: isReverse); _searchTracks(LibraryTab.tracks.textSearchController?.text ?? ''); @@ -622,7 +620,7 @@ class SearchSortController { _sortTracksRaw( sortBy: sortBy, reverse: reverse, - list: trackSearchTemp, + list: trackSearchTemp.value, onDone: (sortType, isReverse) { if (!isAuto) { settings.save(tracksSortSearch: sortType, tracksSortSearchReversed: isReverse); @@ -772,9 +770,7 @@ class SearchSortController { null; } - finalMap.value - ..clear() - ..addEntries(albumsList); + finalMap.value.assignAllEntries(albumsList); settings.save(albumSort: sortBy, albumSortReversed: reverse); @@ -843,9 +839,7 @@ class SearchSortController { default: null; } - finalMap.value - ..clear() - ..addEntries(artistsList); + finalMap.value.assignAllEntries(artistsList); settings.save(artistSort: sortBy, artistSortReversed: reverse); @@ -896,9 +890,7 @@ class SearchSortController { null; } - finalMap.value - ..clear() - ..addEntries(genresList); + finalMap.value.assignAllEntries(genresList); settings.save(genreSort: sortBy, genreSortReversed: reverse); _searchMediaType(type: MediaType.genre, text: LibraryTab.genres.textSearchController?.text ?? ''); @@ -936,9 +928,7 @@ class SearchSortController { null; } - playlistsMap - ..clear() - ..addEntries(playlistList); + playlistsMap.assignAllEntries(playlistList); settings.save(playlistSort: sortBy, playlistSortReversed: reverse); @@ -970,7 +960,7 @@ class SearchSortController { final results = []; if (keyIsPath) { - keys.loop((path, _) { + keys.loop((path) { String pathN = path; while (pathN.isNotEmpty && pathN[pathN.length - 1] == Platform.pathSeparator) { pathN = pathN.substring(0, pathN.length); @@ -980,7 +970,7 @@ class SearchSortController { } }); } else { - keys.loop((name, _) { + keys.loop((name) { if (cleanupFunction(name).contains(cleanupFunction(text))) { results.add(name); } diff --git a/lib/controller/selected_tracks_controller.dart b/lib/controller/selected_tracks_controller.dart index 29e14a47..ba97520f 100644 --- a/lib/controller/selected_tracks_controller.dart +++ b/lib/controller/selected_tracks_controller.dart @@ -1,6 +1,8 @@ +// ignore_for_file: avoid_rx_value_getter_outside_obx import 'dart:io'; -import 'package:get/get.dart'; +import 'package:namida/class/route.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/track.dart'; import 'package:namida/controller/miniplayer_controller.dart'; @@ -17,29 +19,29 @@ class SelectedTracksController { static final SelectedTracksController _instance = SelectedTracksController._internal(); SelectedTracksController._internal(); - List get selectedTracks => _tracksOrTwdList; + RxBaseCore> get selectedTracks => _tracksOrTwdList; final selectedPlaylistsNames = {}; final _tracksOrTwdList = [].obs; final _allTracksHashCodes = {}.obs; - List get currentAllTracks { + Iterable getCurrentAllTracks() { if (MiniPlayerController.inst.isInQueue) { - return Player.inst.currentQueue; + return Player.inst.currentQueue.value.mapAs(); } else if (ScrollSearchController.inst.isGlobalSearchMenuShown.value) { - return SearchSortController.inst.trackSearchTemp; + return SearchSortController.inst.trackSearchTemp.value; } - return NamidaNavigator.inst.currentRoute?.tracksInside ?? []; + return NamidaNavigator.inst.currentRoute?.tracksInside() ?? []; } - final RxBool isMenuMinimized = true.obs; - final RxBool isExpanded = false.obs; + final isMenuMinimized = true.obs; + final isExpanded = false.obs; - final RxBool didInsertTracks = false.obs; + final didInsertTracks = false.obs; - final RxDouble bottomPadding = 0.0.obs; + final bottomPadding = 0.0.obs; // bool isTrackSelected(Selectable twd) => _tracksOrTwdList.contains(twd); bool isTrackSelected(Selectable twd) => _allTracksHashCodes[twd.track] != null; @@ -49,7 +51,7 @@ class SelectedTracksController { final rawTrack = track.track; if (isTrackSelected(track)) { _allTracksHashCodes.remove(rawTrack); - final indexInList = _tracksOrTwdList.indexWhere((element) => element.track == rawTrack); + final indexInList = _tracksOrTwdList.value.indexWhere((element) => element.track == rawTrack); if (indexInList != -1) _tracksOrTwdList.removeAt(indexInList); selectedPlaylistsNames.remove(rawTrack); } else { @@ -62,11 +64,9 @@ class SelectedTracksController { } void reorderTracks(int oldIndex, int newIndex) { - if (newIndex > oldIndex) { - newIndex -= 1; - } - final item = _tracksOrTwdList.removeAt(oldIndex); + if (newIndex > oldIndex) newIndex -= 1; + final item = _tracksOrTwdList.value.removeAt(oldIndex); _tracksOrTwdList.insertSafe(newIndex, item); } @@ -85,21 +85,55 @@ class SelectedTracksController { } void selectAllTracks() { - final tracks = currentAllTracks; - _tracksOrTwdList.addAll(tracks); - _tracksOrTwdList.removeDuplicates((element) => element.track); - tracks.loop((e, index) => _allTracksHashCodes[e.track] = true); + List? tracks; + NamidaRoute? routeTracks; // if the tracks are obtained from route + if (MiniPlayerController.inst.isInQueue) { + tracks = Player.inst.currentQueue.value.mapAs().toList(); + } else if (ScrollSearchController.inst.isGlobalSearchMenuShown.value) { + tracks = SearchSortController.inst.trackSearchTemp.value; + } else { + final currentRoute = NamidaNavigator.inst.currentRoute; + tracks = currentRoute?.tracksListInside(); + routeTracks = currentRoute; + } + + if (tracks == null || tracks.isEmpty) return; + + String? playlistNameToAdd; // -- Adding playlist name if the current route is referring to a playlist - final cr = NamidaNavigator.inst.currentRoute; - if (cr?.route != null) { - if (cr!.route == RouteType.SUBPAGE_playlistTracks || cr.route == RouteType.SUBPAGE_historyTracks) { - cr.tracksInside.loop((e, index) { - selectedPlaylistsNames[e.track] = cr.name; - }); + final cr = routeTracks; + if (cr != null) { + if (cr.route == RouteType.SUBPAGE_playlistTracks || cr.route == RouteType.SUBPAGE_historyTracks) { + playlistNameToAdd = cr.name; } } // ---------- + + final pln = playlistNameToAdd; + final hashMap = _allTracksHashCodes.value; + final trMainList = _tracksOrTwdList.value; + if (pln != null && pln != '') { + tracks.loop((twd) { + final tr = twd.track; + if (hashMap[tr] != true) { + trMainList.add(twd); + hashMap[tr] = true; + selectedPlaylistsNames[tr] = pln; // <-- difference here + } + }); + } else { + tracks.loop((twd) { + final tr = twd.track; + if (hashMap[tr] != true) { + trMainList.add(twd); + hashMap[tr] = true; + } + }); + } + + _allTracksHashCodes.refresh(); + _tracksOrTwdList.refresh(); } void replaceThisTrack(Track oldTrack, Track newTrack) { @@ -135,7 +169,7 @@ class SelectedTracksController { }, ); _allTracksHashCodes.clear(); - _tracksOrTwdList.loop((e, index) { + _tracksOrTwdList.value.loop((e) { _allTracksHashCodes[e.track] = true; }); } diff --git a/lib/controller/settings.equalizer.dart b/lib/controller/settings.equalizer.dart index 615bd8d7..08155743 100644 --- a/lib/controller/settings.equalizer.dart +++ b/lib/controller/settings.equalizer.dart @@ -1,8 +1,7 @@ -import 'package:get/get_rx/src/rx_types/rx_types.dart'; - import 'package:namida/base/settings_file_writer.dart'; import 'package:namida/core/constants.dart'; import 'package:namida/core/extensions.dart'; +import 'package:namida/core/utils.dart'; class EqualizerSettings with SettingsFileWriter { static final EqualizerSettings inst = EqualizerSettings._internal(); @@ -14,7 +13,7 @@ class EqualizerSettings with SettingsFileWriter { bool loudnessEnhancerEnabled = false; double loudnessEnhancer = 0.0; - final uiTapToUpdate = true.obs; + final uiTapToUpdate = true.obso; void save({ int? preset, diff --git a/lib/controller/settings.player.dart b/lib/controller/settings.player.dart index 7394ca03..74309b1a 100644 --- a/lib/controller/settings.player.dart +++ b/lib/controller/settings.player.dart @@ -1,7 +1,6 @@ // ignore_for_file: invalid_use_of_protected_member -import 'package:get/get.dart'; -import 'package:get/get_rx/src/rx_types/rx_types.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/base/settings_file_writer.dart'; import 'package:namida/core/constants.dart'; @@ -170,8 +169,7 @@ class PlayerSettings with SettingsFileWriter { onInterrupted.map((key, value) => MapEntry(key, value)); final lpi = json['lastPlayedIndices']; if (lpi is Map) { - lastPlayedIndices.clear(); - lastPlayedIndices.addAll(lpi.cast()); + lastPlayedIndices.assignAll(lpi.cast()); } } catch (e) { printy(e, isError: true); diff --git a/lib/controller/settings_controller.dart b/lib/controller/settings_controller.dart index b72339af..080ddcdd 100644 --- a/lib/controller/settings_controller.dart +++ b/lib/controller/settings_controller.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:history_manager/history_manager.dart'; import 'package:namida/base/settings_file_writer.dart'; @@ -22,15 +22,15 @@ class SettingsController with SettingsFileWriter { EqualizerSettings get equalizer => EqualizerSettings.inst; PlayerSettings get player => PlayerSettings.inst; - final Rx selectedLanguage = kDefaultLang.obs; - final Rx themeMode = ThemeMode.system.obs; - final RxBool pitchBlack = false.obs; - final RxBool autoColor = true.obs; - final RxInt staticColor = kMainColorLight.value.obs; - final RxInt staticColorDark = kMainColorDark.value.obs; - final Rx selectedLibraryTab = LibraryTab.tracks.obs; - final Rx staticLibraryTab = LibraryTab.tracks.obs; - final RxBool autoLibraryTab = true.obs; + final selectedLanguage = kDefaultLang.obs; + final themeMode = ThemeMode.system.obs; + final pitchBlack = false.obs; + final autoColor = true.obs; + final staticColor = kMainColorLight.value.obs; + final staticColorDark = kMainColorDark.value.obs; + final selectedLibraryTab = LibraryTab.tracks.obs; + final staticLibraryTab = LibraryTab.tracks.obs; + final autoLibraryTab = true.obs; final RxList libraryTabs = [ LibraryTab.home, LibraryTab.tracks, @@ -39,69 +39,69 @@ class SettingsController with SettingsFileWriter { LibraryTab.folders, LibraryTab.youtube, ].obs; - final RxInt searchResultsPlayMode = 1.obs; - final RxDouble borderRadiusMultiplier = 1.0.obs; - final RxDouble fontScaleFactor = 0.9.obs; - final RxDouble artworkCacheHeightMultiplier = 0.8.obs; - final RxDouble trackThumbnailSizeinList = 70.0.obs; - final RxDouble trackListTileHeight = 70.0.obs; - final RxDouble albumThumbnailSizeinList = 90.0.obs; - final RxDouble albumListTileHeight = 90.0.obs; - - final RxBool useMediaStore = false.obs; - final RxBool displayTrackNumberinAlbumPage = true.obs; - final RxBool albumCardTopRightDate = true.obs; - final RxBool forceSquaredTrackThumbnail = false.obs; - final RxBool forceSquaredAlbumThumbnail = false.obs; - final RxBool useAlbumStaggeredGridView = false.obs; - final RxBool useSettingCollapsedTiles = true.obs; - final RxInt albumGridCount = 2.obs; - final RxInt artistGridCount = 3.obs; - final RxInt genreGridCount = 2.obs; - final RxInt playlistGridCount = 1.obs; - final RxBool enableBlurEffect = false.obs; - final RxBool enableGlowEffect = false.obs; - final RxBool hourFormat12 = true.obs; - final RxString dateTimeFormat = 'MMM yyyy'.obs; + final searchResultsPlayMode = 1.obs; + final borderRadiusMultiplier = 1.0.obs; + final fontScaleFactor = 0.9.obs; + final artworkCacheHeightMultiplier = 0.8.obs; + final trackThumbnailSizeinList = 70.0.obs; + final trackListTileHeight = 70.0.obs; + final albumThumbnailSizeinList = 90.0.obs; + final albumListTileHeight = 90.0.obs; + + final useMediaStore = false.obs; + final displayTrackNumberinAlbumPage = true.obs; + final albumCardTopRightDate = true.obs; + final forceSquaredTrackThumbnail = false.obs; + final forceSquaredAlbumThumbnail = false.obs; + final useAlbumStaggeredGridView = false.obs; + final useSettingCollapsedTiles = true.obs; + final albumGridCount = 2.obs; + final artistGridCount = 3.obs; + final genreGridCount = 2.obs; + final playlistGridCount = 1.obs; + final enableBlurEffect = false.obs; + final enableGlowEffect = false.obs; + final hourFormat12 = true.obs; + final dateTimeFormat = 'MMM yyyy'.obs; final RxList trackArtistsSeparators = ['&', ',', ';', '//', ' ft. ', ' x '].obs; final RxList trackGenresSeparators = ['&', ',', ';', '//', ' x '].obs; final RxList trackArtistsSeparatorsBlacklist = [].obs; final RxList trackGenresSeparatorsBlacklist = [].obs; - final Rx tracksSort = SortType.title.obs; - final RxBool tracksSortReversed = false.obs; - final Rx tracksSortSearch = SortType.title.obs; - final RxBool tracksSortSearchReversed = false.obs; - final RxBool tracksSortSearchIsAuto = true.obs; - final Rx albumSort = GroupSortType.album.obs; - final RxBool albumSortReversed = false.obs; - final Rx artistSort = GroupSortType.artistsList.obs; - final RxBool artistSortReversed = false.obs; - final Rx genreSort = GroupSortType.genresList.obs; - final RxBool genreSortReversed = false.obs; - final Rx playlistSort = GroupSortType.dateModified.obs; - final RxBool playlistSortReversed = false.obs; - final Rx ytPlaylistSort = GroupSortType.dateModified.obs; - final RxBool ytPlaylistSortReversed = true.obs; - final RxInt indexMinDurationInSec = 5.obs; - final RxInt indexMinFileSizeInB = (100 * 1024).obs; + final tracksSort = SortType.title.obs; + final tracksSortReversed = false.obs; + final tracksSortSearch = SortType.title.obs; + final tracksSortSearchReversed = false.obs; + final tracksSortSearchIsAuto = true.obs; + final albumSort = GroupSortType.album.obs; + final albumSortReversed = false.obs; + final artistSort = GroupSortType.artistsList.obs; + final artistSortReversed = false.obs; + final genreSort = GroupSortType.genresList.obs; + final genreSortReversed = false.obs; + final playlistSort = GroupSortType.dateModified.obs; + final playlistSortReversed = false.obs; + final ytPlaylistSort = GroupSortType.dateModified.obs; + final ytPlaylistSortReversed = true.obs; + final indexMinDurationInSec = 5.obs; + final indexMinFileSizeInB = (100 * 1024).obs; final RxList trackSearchFilter = [ TrackSearchFilter.filename, TrackSearchFilter.title, TrackSearchFilter.artist, TrackSearchFilter.album, ].obs; - final RxList playlistSearchFilter = ['name', 'creationDate', 'modifiedDate', 'moods', 'comment'].obs; - final RxList directoriesToScan = kInitialDirectoriesToScan.toList().obs; - final RxList directoriesToExclude = [].obs; - final RxBool preventDuplicatedTracks = false.obs; - final RxBool respectNoMedia = false.obs; - final RxString defaultBackupLocation = AppDirs.BACKUPS.obs; - final RxInt autoBackupIntervalDays = 2.obs; - final RxString defaultFolderStartupLocation = kStoragePaths.first.obs; - final RxString ytDownloadLocation = AppDirs.YOUTUBE_DOWNLOADS_DEFAULT.obs; - final RxBool enableFoldersHierarchy = true.obs; - final RxBool displayArtistBeforeTitle = true.obs; - final RxBool heatmapListensView = false.obs; + final playlistSearchFilter = ['name', 'creationDate', 'modifiedDate', 'moods', 'comment'].obs; + final directoriesToScan = kInitialDirectoriesToScan.toList().obs; + final directoriesToExclude = [].obs; + final preventDuplicatedTracks = false.obs; + final respectNoMedia = false.obs; + final defaultBackupLocation = AppDirs.BACKUPS.obs; + final autoBackupIntervalDays = 2.obs; + final defaultFolderStartupLocation = kStoragePaths.first.obs; + final ytDownloadLocation = AppDirs.YOUTUBE_DOWNLOADS_DEFAULT.obs; + final enableFoldersHierarchy = true.obs; + final displayArtistBeforeTitle = true.obs; + final heatmapListensView = false.obs; final RxList backupItemslist = [ AppPaths.TRACKS, AppPaths.TRACKS_STATS, @@ -121,50 +121,50 @@ class SettingsController with SettingsFileWriter { AppDirs.YT_HISTORY_PLAYLIST, AppPaths.YT_LIKES_PLAYLIST, ].obs; - final RxBool enableVideoPlayback = true.obs; - final RxBool enableLyrics = false.obs; - final Rx lyricsSource = LyricsSource.auto.obs; - final Rx videoPlaybackSource = VideoPlaybackSource.auto.obs; + final enableVideoPlayback = true.obs; + final enableLyrics = false.obs; + final lyricsSource = LyricsSource.auto.obs; + final videoPlaybackSource = VideoPlaybackSource.auto.obs; final RxList youtubeVideoQualities = ['480p', '360p', '240p', '144p'].obs; - final RxDouble animatingThumbnailScaleMultiplier = 1.0.obs; - final RxInt animatingThumbnailIntensity = 25.obs; - final RxBool animatingThumbnailInversed = false.obs; - final RxBool enablePartyModeInMiniplayer = false.obs; - final RxBool enablePartyModeColorSwap = true.obs; - final RxBool enableMiniplayerParticles = true.obs; - final RxBool enableMiniplayerParallaxEffect = true.obs; - final RxBool forceMiniplayerTrackColor = false.obs; - final RxInt isTrackPlayedSecondsCount = 40.obs; - final RxInt isTrackPlayedPercentageCount = 40.obs; - final RxInt waveformTotalBars = 140.obs; - final RxInt videosMaxCacheInMB = (2 * 1024).obs; // 2GB - final RxInt audiosMaxCacheInMB = (2 * 1024).obs; // 2GB - final RxInt imagesMaxCacheInMB = (8 * 32).obs; // 256 MB - final RxInt ytMiniplayerDimAfterSeconds = 15.obs; - final RxDouble ytMiniplayerDimOpacity = 0.5.obs; - final RxBool youtubeStyleMiniplayer = true.obs; - final RxBool hideStatusBarInExpandedMiniplayer = false.obs; - final RxBool displayFavouriteButtonInNotification = false.obs; - final RxBool enableSearchCleanup = true.obs; - final RxBool enableBottomNavBar = true.obs; - final RxBool ytPreferNewComments = false.obs; - final RxBool ytAutoExtractVideoTagsFromInfo = true.obs; - final RxBool displayAudioInfoMiniplayer = false.obs; - final RxBool showUnknownFieldsInTrackInfoDialog = true.obs; - final RxBool extractFeatArtistFromTitle = true.obs; - final RxBool groupArtworksByAlbum = false.obs; - final RxBool enableM3USync = false.obs; - final RxBool prioritizeEmbeddedLyrics = true.obs; - final RxBool swipeableDrawer = true.obs; - final RxBool dismissibleMiniplayer = false.obs; - final RxBool enableClipboardMonitoring = false.obs; - final RxBool ytIsAudioOnlyMode = false.obs; - final RxBool ytRememberAudioOnly = false.obs; - final RxBool ytTopComments = true.obs; - final RxBool artworkGestureScale = false.obs; - final RxBool artworkGestureDoubleTapLRC = true.obs; - final RxBool previousButtonReplays = false.obs; - final RxBool refreshOnStartup = false.obs; + final animatingThumbnailScaleMultiplier = 1.0.obs; + final animatingThumbnailIntensity = 25.obs; + final animatingThumbnailInversed = false.obs; + final enablePartyModeInMiniplayer = false.obs; + final enablePartyModeColorSwap = true.obs; + final enableMiniplayerParticles = true.obs; + final enableMiniplayerParallaxEffect = true.obs; + final forceMiniplayerTrackColor = false.obs; + final isTrackPlayedSecondsCount = 40.obs; + final isTrackPlayedPercentageCount = 40.obs; + final waveformTotalBars = 140.obs; + final videosMaxCacheInMB = (2 * 1024).obs; // 2GB + final audiosMaxCacheInMB = (2 * 1024).obs; // 2GB + final imagesMaxCacheInMB = (8 * 32).obs; // 256 MB + final ytMiniplayerDimAfterSeconds = 15.obs; + final ytMiniplayerDimOpacity = 0.5.obs; + final youtubeStyleMiniplayer = true.obs; + final hideStatusBarInExpandedMiniplayer = false.obs; + final displayFavouriteButtonInNotification = false.obs; + final enableSearchCleanup = true.obs; + final enableBottomNavBar = true.obs; + final ytPreferNewComments = false.obs; + final ytAutoExtractVideoTagsFromInfo = true.obs; + final displayAudioInfoMiniplayer = false.obs; + final showUnknownFieldsInTrackInfoDialog = true.obs; + final extractFeatArtistFromTitle = true.obs; + final groupArtworksByAlbum = false.obs; + final enableM3USync = false.obs; + final prioritizeEmbeddedLyrics = true.obs; + final swipeableDrawer = true.obs; + final dismissibleMiniplayer = false.obs; + final enableClipboardMonitoring = false.obs; + final ytIsAudioOnlyMode = false.obs; + final ytRememberAudioOnly = false.obs; + final ytTopComments = true.obs; + final artworkGestureScale = false.obs; + final artworkGestureDoubleTapLRC = true.obs; + final previousButtonReplays = false.obs; + final refreshOnStartup = false.obs; final RxList tagFieldsToEdit = [ TagField.trackNumber, TagField.year, @@ -178,12 +178,12 @@ class SettingsController with SettingsFileWriter { TagField.lyrics, ].obs; - final Rx wakelockMode = WakelockMode.expandedAndVideo.obs; + final wakelockMode = WakelockMode.expandedAndVideo.obs; - final Rx localVideoMatchingType = LocalVideoMatchingType.auto.obs; - final RxBool localVideoMatchingCheckSameDir = false.obs; + final localVideoMatchingType = LocalVideoMatchingType.auto.obs; + final localVideoMatchingCheckSameDir = false.obs; - final Rx trackPlayMode = TrackPlayMode.searchResults.obs; + final trackPlayMode = TrackPlayMode.searchResults.obs; final mostPlayedTimeRange = MostPlayedTimeRange.allTime.obs; final mostPlayedCustomDateRange = DateRange.dummy().obs; @@ -194,15 +194,15 @@ class SettingsController with SettingsFileWriter { final ytMostPlayedCustomisStartOfDay = true.obs; /// Track Items - final RxBool displayThirdRow = true.obs; - final RxBool displayThirdItemInEachRow = false.obs; - final RxString trackTileSeparator = '•'.obs; - final RxBool displayFavouriteIconInListTile = true.obs; - final RxBool editTagsKeepFileDates = true.obs; - final RxBool downloadFilesWriteUploadDate = true.obs; - final RxBool downloadFilesKeepCachedVersions = true.obs; - final RxBool enablePip = true.obs; - final RxBool pickColorsFromDeviceWallpaper = false.obs; + final displayThirdRow = true.obs; + final displayThirdItemInEachRow = false.obs; + final trackTileSeparator = '•'.obs; + final displayFavouriteIconInListTile = true.obs; + final editTagsKeepFileDates = true.obs; + final downloadFilesWriteUploadDate = true.obs; + final downloadFilesKeepCachedVersions = true.obs; + final enablePip = true.obs; + final pickColorsFromDeviceWallpaper = false.obs; final onNotificationTapAction = NotificationTapAction.openApp.obs; final onYoutubeLinkOpen = OnYoutubeLinkOpenAction.alwaysAsk.obs; final performanceMode = PerformanceMode.balanced.obs; @@ -211,7 +211,7 @@ class SettingsController with SettingsFileWriter { final ytTapToSeek = YTSeekActionMode.expandedMiniplayer.obs; final ytDragToSeek = YTSeekActionMode.all.obs; - final RxMap trackItem = { + final trackItem = { TrackTilePosition.row1Item1: TrackTileItem.title, TrackTilePosition.row1Item2: TrackTileItem.none, TrackTilePosition.row1Item3: TrackTileItem.none, @@ -245,7 +245,7 @@ class SettingsController with SettingsFileWriter { HomePageItems.recentlyAdded, HomePageItems.recentAlbums, HomePageItems.recentArtists, - ].obs; + ].obso; final activeArtistType = MediaType.artist.obs; @@ -296,24 +296,19 @@ class SettingsController with SettingsFileWriter { : LibraryTab.values.getEnum(json['staticLibraryTab']) ?? staticLibraryTab.value; staticLibraryTab.value = LibraryTab.values.getEnum(json['staticLibraryTab']) ?? staticLibraryTab.value; autoLibraryTab.value = json['autoLibraryTab'] ?? autoLibraryTab.value; - final libraryListFromStorage = List.from(json['libraryTabs'] ?? []); - libraryTabs.value = libraryListFromStorage.isNotEmpty ? List.from(libraryListFromStorage.map((e) => LibraryTab.values.getEnum(e))) : libraryTabs.toList(); + final libraryListFromStorage = json['libraryTabs']; + if (libraryListFromStorage is List) libraryTabs.value = libraryListFromStorage.map((e) => LibraryTab.values.getEnum(e)).toListy(); - final homePageItemsFromStorage = List.from(json['homePageItems'] ?? []); - homePageItems.value = - homePageItemsFromStorage.isNotEmpty ? List.from(homePageItemsFromStorage.map((e) => HomePageItems.values.getEnum(e))) : homePageItems.toList(); + final homePageItemsFromStorage = json['homePageItems']; + if (homePageItemsFromStorage is List) homePageItems.value = homePageItemsFromStorage.map((e) => HomePageItems.values.getEnum(e)).toListy(); activeArtistType.value = MediaType.values.getEnum(json['activeArtistType']) ?? activeArtistType.value; - final activeSearchMediaTypesFromStorage = List.from(json['activeSearchMediaTypes'] ?? []); - activeSearchMediaTypes.value = activeSearchMediaTypesFromStorage.isNotEmpty - ? List.from(activeSearchMediaTypesFromStorage.map((e) => MediaType.values.getEnum(e))) - : activeSearchMediaTypes.toList(); + final activeSearchMediaTypesFromStorage = json['activeSearchMediaTypes']; + if (activeSearchMediaTypesFromStorage is List) activeSearchMediaTypes.value = activeSearchMediaTypesFromStorage.map((e) => MediaType.values.getEnum(e)).toListy(); - final albumIdentifiersFromStorage = List.from(json['albumIdentifiers'] ?? []); - albumIdentifiers.value = albumIdentifiersFromStorage.isNotEmpty - ? List.from(albumIdentifiersFromStorage.map((e) => AlbumIdentifier.values.getEnum(e))) - : albumIdentifiers.toList(); + final albumIdentifiersFromStorage = json['albumIdentifiers']; + if (albumIdentifiersFromStorage is List) albumIdentifiers.value = albumIdentifiersFromStorage.map((e) => AlbumIdentifier.values.getEnum(e)).toListy(); searchResultsPlayMode.value = json['searchResultsPlayMode'] ?? searchResultsPlayMode.value; borderRadiusMultiplier.value = json['borderRadiusMultiplier'] ?? borderRadiusMultiplier.value; @@ -340,10 +335,10 @@ class SettingsController with SettingsFileWriter { hourFormat12.value = json['hourFormat12'] ?? hourFormat12.value; dateTimeFormat.value = json['dateTimeFormat'] ?? dateTimeFormat.value; - trackArtistsSeparators.value = List.from(json['trackArtistsSeparators'] ?? trackArtistsSeparators); - trackGenresSeparators.value = List.from(json['trackGenresSeparators'] ?? trackGenresSeparators); - trackArtistsSeparatorsBlacklist.value = List.from(json['trackArtistsSeparatorsBlacklist'] ?? trackArtistsSeparatorsBlacklist); - trackGenresSeparatorsBlacklist.value = List.from(json['trackGenresSeparatorsBlacklist'] ?? trackGenresSeparatorsBlacklist); + if (json['trackArtistsSeparators'] is List) trackArtistsSeparators.value = (json['trackArtistsSeparators'] as List).cast(); + if (json['trackGenresSeparators'] is List) trackGenresSeparators.value = (json['trackGenresSeparators'] as List).cast(); + if (json['trackArtistsSeparatorsBlacklist'] is List) trackArtistsSeparatorsBlacklist.value = (json['trackArtistsSeparatorsBlacklist'] as List).cast(); + if (json['trackGenresSeparatorsBlacklist'] is List) trackGenresSeparatorsBlacklist.value = (json['trackGenresSeparatorsBlacklist'] as List).cast(); tracksSort.value = SortType.values.getEnum(json['tracksSort']) ?? tracksSort.value; tracksSortReversed.value = json['tracksSortReversed'] ?? tracksSortReversed.value; tracksSortSearch.value = SortType.values.getEnum(json['tracksSortSearch']) ?? tracksSortSearch.value; @@ -364,15 +359,15 @@ class SettingsController with SettingsFileWriter { try { // -- backward compability, since the previous type was String - final trackSearchFilterInStorage = List.from(json['trackSearchFilter'] ?? []); - if (trackSearchFilterInStorage.isNotEmpty) { - trackSearchFilter.value = List.from(trackSearchFilterInStorage.map((e) => TrackSearchFilter.values.getEnum(e))); + final trackSearchFilterInStorage = json['trackSearchFilter']; + if (trackSearchFilterInStorage is List) { + trackSearchFilter.value = trackSearchFilterInStorage.map((e) => TrackSearchFilter.values.getEnum(e)).toListy(); } } catch (_) {} - playlistSearchFilter.value = List.from(json['playlistSearchFilter'] ?? playlistSearchFilter); - directoriesToScan.value = List.from(json['directoriesToScan'] ?? directoriesToScan); - directoriesToExclude.value = List.from(json['directoriesToExclude'] ?? directoriesToExclude); + if (json['playlistSearchFilter'] is List) playlistSearchFilter.value = (json['playlistSearchFilter'] as List).cast(); + if (json['directoriesToScan'] is List) directoriesToScan.value = (json['directoriesToScan'] as List).cast(); + if (json['directoriesToExclude'] is List) directoriesToExclude.value = (json['directoriesToExclude'] as List).cast(); preventDuplicatedTracks.value = json['preventDuplicatedTracks'] ?? preventDuplicatedTracks.value; respectNoMedia.value = json['respectNoMedia'] ?? respectNoMedia.value; defaultBackupLocation.value = json['defaultBackupLocation'] ?? defaultBackupLocation.value; @@ -382,12 +377,12 @@ class SettingsController with SettingsFileWriter { enableFoldersHierarchy.value = json['enableFoldersHierarchy'] ?? enableFoldersHierarchy.value; displayArtistBeforeTitle.value = json['displayArtistBeforeTitle'] ?? displayArtistBeforeTitle.value; heatmapListensView.value = json['heatmapListensView'] ?? heatmapListensView.value; - backupItemslist.value = List.from(json['backupItemslist'] ?? backupItemslist); + if (json['backupItemslist'] is List) backupItemslist.value = (json['backupItemslist'] as List).cast(); enableVideoPlayback.value = json['enableVideoPlayback'] ?? enableVideoPlayback.value; enableLyrics.value = json['enableLyrics'] ?? enableLyrics.value; lyricsSource.value = LyricsSource.values.getEnum(json['lyricsSource']) ?? lyricsSource.value; videoPlaybackSource.value = VideoPlaybackSource.values.getEnum(json['videoPlaybackSource']) ?? videoPlaybackSource.value; - youtubeVideoQualities.value = List.from(json['youtubeVideoQualities'] ?? youtubeVideoQualities); + if (json['youtubeVideoQualities'] is List) youtubeVideoQualities.value = (json['youtubeVideoQualities'] as List).cast(); animatingThumbnailScaleMultiplier.value = json['animatingThumbnailScaleMultiplier'] ?? animatingThumbnailScaleMultiplier.value; animatingThumbnailIntensity.value = json['animatingThumbnailIntensity'] ?? animatingThumbnailIntensity.value; @@ -429,8 +424,10 @@ class SettingsController with SettingsFileWriter { previousButtonReplays.value = json['previousButtonReplays'] ?? previousButtonReplays.value; refreshOnStartup.value = json['refreshOnStartup'] ?? refreshOnStartup.value; - final listFromStorage = List.from(json['tagFieldsToEdit'] ?? []); - tagFieldsToEdit.value = listFromStorage.isNotEmpty ? List.from(listFromStorage.map((e) => TagField.values.getEnum(e))) : tagFieldsToEdit; + final tagFieldsToEditStorage = json['tagFieldsToEdit']; + if (tagFieldsToEditStorage is List) { + tagFieldsToEdit.value = tagFieldsToEditStorage.map((e) => TagField.values.getEnum(e as String)).toListy(); + } wakelockMode.value = WakelockMode.values.getEnum(json['wakelockMode']) ?? wakelockMode.value; @@ -472,12 +469,12 @@ class SettingsController with SettingsFileWriter { TrackTileItem.values, TrackTileItem.none, ) ?? - trackItem.map((key, value) => MapEntry(key, value)); + trackItem.value.map((key, value) => MapEntry(key, value)); queueInsertion.value = ((json["queueInsertion"] as Map?)?.map( (key, value) => MapEntry(QueueInsertionType.values.getEnum(key) ?? QueueInsertionType.moreAlbum, QueueInsertion.fromJson(value)), )) ?? - queueInsertion.map((key, value) => MapEntry(key, value)); + queueInsertion.value.map((key, value) => MapEntry(key, value)); final mediaItemsTrackSortingInStorage = json["mediaItemsTrackSorting"] as Map? ?? {}; mediaItemsTrackSorting.value = { @@ -498,7 +495,7 @@ class SettingsController with SettingsFileWriter { @override Object get jsonToWrite => { - 'selectedLanguage': selectedLanguage.toJson(), + 'selectedLanguage': selectedLanguage.value.toJson(), 'themeMode': themeMode.value.convertToString, 'pitchBlack': pitchBlack.value, 'autoColor': autoColor.value, @@ -536,10 +533,10 @@ class SettingsController with SettingsFileWriter { 'enableGlowEffect': enableGlowEffect.value, 'hourFormat12': hourFormat12.value, 'dateTimeFormat': dateTimeFormat.value, - 'trackArtistsSeparators': trackArtistsSeparators.toList(), - 'trackGenresSeparators': trackGenresSeparators.toList(), - 'trackArtistsSeparatorsBlacklist': trackArtistsSeparatorsBlacklist.toList(), - 'trackGenresSeparatorsBlacklist': trackGenresSeparatorsBlacklist.toList(), + 'trackArtistsSeparators': trackArtistsSeparators.value, + 'trackGenresSeparators': trackGenresSeparators.value, + 'trackArtistsSeparatorsBlacklist': trackArtistsSeparatorsBlacklist.value, + 'trackGenresSeparatorsBlacklist': trackGenresSeparatorsBlacklist.value, 'tracksSort': tracksSort.value.convertToString, 'tracksSortReversed': tracksSortReversed.value, 'tracksSortSearch': tracksSortSearch.value.convertToString, @@ -558,9 +555,9 @@ class SettingsController with SettingsFileWriter { 'indexMinDurationInSec': indexMinDurationInSec.value, 'indexMinFileSizeInB': indexMinFileSizeInB.value, 'trackSearchFilter': trackSearchFilter.mapped((e) => e.convertToString), - 'playlistSearchFilter': playlistSearchFilter.toList(), - 'directoriesToScan': directoriesToScan.toList(), - 'directoriesToExclude': directoriesToExclude.toList(), + 'playlistSearchFilter': playlistSearchFilter.value, + 'directoriesToScan': directoriesToScan.value, + 'directoriesToExclude': directoriesToExclude.value, 'preventDuplicatedTracks': preventDuplicatedTracks.value, 'respectNoMedia': respectNoMedia.value, 'defaultBackupLocation': defaultBackupLocation.value, @@ -570,12 +567,12 @@ class SettingsController with SettingsFileWriter { 'enableFoldersHierarchy': enableFoldersHierarchy.value, 'displayArtistBeforeTitle': displayArtistBeforeTitle.value, 'heatmapListensView': heatmapListensView.value, - 'backupItemslist': backupItemslist.toList(), + 'backupItemslist': backupItemslist.value, 'enableVideoPlayback': enableVideoPlayback.value, 'enableLyrics': enableLyrics.value, 'lyricsSource': lyricsSource.value.convertToString, 'videoPlaybackSource': videoPlaybackSource.value.convertToString, - 'youtubeVideoQualities': youtubeVideoQualities.toList(), + 'youtubeVideoQualities': youtubeVideoQualities.value, 'animatingThumbnailScaleMultiplier': animatingThumbnailScaleMultiplier.value, 'animatingThumbnailIntensity': animatingThumbnailIntensity.value, 'animatingThumbnailInversed': animatingThumbnailInversed.value, @@ -644,10 +641,10 @@ class SettingsController with SettingsFileWriter { 'downloadFilesKeepCachedVersions': downloadFilesKeepCachedVersions.value, 'enablePip': enablePip.value, 'pickColorsFromDeviceWallpaper': pickColorsFromDeviceWallpaper.value, - 'trackItem': trackItem.map((key, value) => MapEntry(key.convertToString, value.convertToString)), - 'queueInsertion': queueInsertion.map((key, value) => MapEntry(key.convertToString, value.toJson())), - 'mediaItemsTrackSorting': mediaItemsTrackSorting.map((key, value) => MapEntry(key.convertToString, value.map((e) => e.convertToString).toList())), - 'mediaItemsTrackSortingReverse': mediaItemsTrackSortingReverse.map((key, value) => MapEntry(key.convertToString, value)), + 'trackItem': trackItem.value.map((key, value) => MapEntry(key.convertToString, value.convertToString)), + 'queueInsertion': queueInsertion.value.map((key, value) => MapEntry(key.convertToString, value.toJson())), + 'mediaItemsTrackSorting': mediaItemsTrackSorting.value.map((key, value) => MapEntry(key.convertToString, value.map((e) => e.convertToString).toList())), + 'mediaItemsTrackSortingReverse': mediaItemsTrackSortingReverse.value.map((key, value) => MapEntry(key.convertToString, value)), 'fontScaleLRC': fontScaleLRC, 'fontScaleLRCFull': fontScaleLRCFull, @@ -819,14 +816,14 @@ class SettingsController with SettingsFileWriter { if (staticLibraryTab != null) this.staticLibraryTab.value = staticLibraryTab; if (autoLibraryTab != null) this.autoLibraryTab.value = autoLibraryTab; if (libraryTabs != null) { - libraryTabs.loop((t, index) { + libraryTabs.loop((t) { if (!this.libraryTabs.contains(t)) { this.libraryTabs.add(t); } }); } if (homePageItems != null) { - homePageItems.loop((t, index) { + homePageItems.loop((t) { if (!this.homePageItems.contains(t)) { this.homePageItems.add(t); } @@ -834,14 +831,14 @@ class SettingsController with SettingsFileWriter { } if (activeArtistType != null) this.activeArtistType.value = activeArtistType; if (activeSearchMediaTypes != null) { - activeSearchMediaTypes.loop((t, index) { + activeSearchMediaTypes.loop((t) { if (!this.activeSearchMediaTypes.contains(t)) { this.activeSearchMediaTypes.add(t); } }); } if (albumIdentifiers != null) { - albumIdentifiers.loop((t, index) { + albumIdentifiers.loop((t) { if (!this.albumIdentifiers.contains(t)) { this.albumIdentifiers.add(t); } @@ -905,28 +902,28 @@ class SettingsController with SettingsFileWriter { if (indexMinDurationInSec != null) this.indexMinDurationInSec.value = indexMinDurationInSec; if (indexMinFileSizeInB != null) this.indexMinFileSizeInB.value = indexMinFileSizeInB; if (trackSearchFilter != null) { - trackSearchFilter.loop((f, index) { + trackSearchFilter.loop((f) { if (!this.trackSearchFilter.contains(f)) { this.trackSearchFilter.add(f); } }); } if (playlistSearchFilter != null) { - playlistSearchFilter.loop((f, index) { + playlistSearchFilter.loop((f) { if (!this.playlistSearchFilter.contains(f)) { this.playlistSearchFilter.add(f); } }); } if (directoriesToScan != null) { - directoriesToScan.loop((d, index) { + directoriesToScan.loop((d) { if (!this.directoriesToScan.contains(d)) { this.directoriesToScan.add(d); } }); } if (directoriesToExclude != null) { - directoriesToExclude.loop((d, index) { + directoriesToExclude.loop((d) { if (!this.directoriesToExclude.contains(d)) { this.directoriesToExclude.add(d); } @@ -942,14 +939,14 @@ class SettingsController with SettingsFileWriter { if (displayArtistBeforeTitle != null) this.displayArtistBeforeTitle.value = displayArtistBeforeTitle; if (heatmapListensView != null) this.heatmapListensView.value = heatmapListensView; if (backupItemslist != null) { - backupItemslist.loop((d, index) { + backupItemslist.loop((d) { if (!this.backupItemslist.contains(d)) { this.backupItemslist.add(d); } }); } if (youtubeVideoQualities != null) { - youtubeVideoQualities.loop((q, index) { + youtubeVideoQualities.loop((q) { if (!this.youtubeVideoQualities.contains(q)) { this.youtubeVideoQualities.add(q); } @@ -1009,7 +1006,7 @@ class SettingsController with SettingsFileWriter { if (previousButtonReplays != null) this.previousButtonReplays.value = previousButtonReplays; if (refreshOnStartup != null) this.refreshOnStartup.value = refreshOnStartup; if (tagFieldsToEdit != null) { - tagFieldsToEdit.loop((d, index) { + tagFieldsToEdit.loop((d) { if (!this.tagFieldsToEdit.contains(d)) { this.tagFieldsToEdit.add(d); } @@ -1090,28 +1087,28 @@ class SettingsController with SettingsFileWriter { if (trackArtistsSeparatorsBlacklist1 != null) trackArtistsSeparatorsBlacklist.remove(trackArtistsSeparatorsBlacklist1); if (trackGenresSeparatorsBlacklist1 != null) trackGenresSeparatorsBlacklist.remove(trackGenresSeparatorsBlacklist1); if (trackSearchFilter1 != null) trackSearchFilter.remove(trackSearchFilter1); - if (trackSearchFilterAll != null) trackSearchFilterAll.loop((f, index) => trackSearchFilter.remove(f)); + if (trackSearchFilterAll != null) trackSearchFilterAll.loop((f) => trackSearchFilter.remove(f)); if (playlistSearchFilter1 != null) playlistSearchFilter.remove(playlistSearchFilter1); if (playlistSearchFilterAll != null) { - playlistSearchFilterAll.loop((f, index) => playlistSearchFilter.remove(f)); + playlistSearchFilterAll.loop((f) => playlistSearchFilter.remove(f)); } if (directoriesToScan1 != null) directoriesToScan.remove(directoriesToScan1); - if (directoriesToScanAll != null) directoriesToScanAll.loop((f, index) => directoriesToScan.remove(f)); + if (directoriesToScanAll != null) directoriesToScanAll.loop((f) => directoriesToScan.remove(f)); if (directoriesToExclude1 != null) directoriesToExclude.remove(directoriesToExclude1); - if (directoriesToExcludeAll != null) directoriesToExcludeAll.loop((f, index) => directoriesToExclude.remove(f)); + if (directoriesToExcludeAll != null) directoriesToExcludeAll.loop((f) => directoriesToExclude.remove(f)); if (libraryTab1 != null) libraryTabs.remove(libraryTab1); - if (libraryTabsAll != null) libraryTabsAll.loop((t, index) => libraryTabs.remove(t)); + if (libraryTabsAll != null) libraryTabsAll.loop((t) => libraryTabs.remove(t)); if (homePageItem1 != null) homePageItems.remove(homePageItem1); - if (homePageItemsAll != null) homePageItemsAll.loop((t, index) => homePageItems.remove(t)); + if (homePageItemsAll != null) homePageItemsAll.loop((t) => homePageItems.remove(t)); if (activeSearchMediaTypes1 != null) activeSearchMediaTypes.remove(activeSearchMediaTypes1); if (albumIdentifiers1 != null) albumIdentifiers.remove(albumIdentifiers1); - if (albumIdentifiersAll != null) albumIdentifiersAll.loop((t, index) => albumIdentifiers.remove(t)); + if (albumIdentifiersAll != null) albumIdentifiersAll.loop((t) => albumIdentifiers.remove(t)); if (backupItemslist1 != null) backupItemslist.remove(backupItemslist1); - if (backupItemslistAll != null) backupItemslistAll.loop((t, index) => backupItemslist.remove(t)); + if (backupItemslistAll != null) backupItemslistAll.loop((t) => backupItemslist.remove(t)); if (youtubeVideoQualities1 != null) youtubeVideoQualities.remove(youtubeVideoQualities1); - if (youtubeVideoQualitiesAll != null) youtubeVideoQualitiesAll.loop((t, index) => youtubeVideoQualities.remove(t)); + if (youtubeVideoQualitiesAll != null) youtubeVideoQualitiesAll.loop((t) => youtubeVideoQualities.remove(t)); if (tagFieldsToEdit1 != null) tagFieldsToEdit.remove(tagFieldsToEdit1); - if (tagFieldsToEditAll != null) tagFieldsToEditAll.loop((t, index) => tagFieldsToEdit.remove(t)); + if (tagFieldsToEditAll != null) tagFieldsToEditAll.loop((t) => tagFieldsToEdit.remove(t)); _writeToStorage(); } @@ -1139,3 +1136,7 @@ class SettingsController with SettingsFileWriter { @override String get filePath => AppPaths.SETTINGS; } + +extension _ListieMapper on Iterable { + List toListy() => whereType().toList(); +} diff --git a/lib/controller/settings_search_controller.dart b/lib/controller/settings_search_controller.dart index b39c6bc7..3d371bbf 100644 --- a/lib/controller/settings_search_controller.dart +++ b/lib/controller/settings_search_controller.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/base/setting_subpage_provider.dart'; import 'package:namida/controller/navigator_controller.dart'; @@ -107,7 +107,7 @@ class SettingsSearchController { final searchResults = >{}.obs; final subpagesDetails = {}; - bool get canShowSearch => _canShowSearch.value; + RxBaseCore get canShowSearch => _canShowSearch; final _canShowSearch = false.obs; void closeSearch() { @@ -156,7 +156,7 @@ class SettingsSearchController { void onSearchChanged(String val) { final res = >{}; - _allWidgets.loop((widget, index) { + _allWidgets.loop((widget) { for (final e in widget.$2.entries) { final match = e.value.any((element) => element.cleanUpForComparison.contains(val.cleanUpForComparison)); if (match) { diff --git a/lib/controller/storage_cache_manager.dart b/lib/controller/storage_cache_manager.dart index caa2bb85..5cd5bbff 100644 --- a/lib/controller/storage_cache_manager.dart +++ b/lib/controller/storage_cache_manager.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:checkmark/checkmark.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart' hide Response; import 'package:jiffy/jiffy.dart'; import 'package:namida/class/track.dart'; @@ -15,6 +14,7 @@ import 'package:namida/core/constants.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/artwork.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; @@ -111,13 +111,13 @@ class StorageCacheManager { final localIdTrackMap = {}; if (includeLocalTracksListens) { - allTracksInLibrary.loop((tr, _) => localIdTrackMap[tr.youtubeID] = tr); + allTracksInLibrary.loop((tr) => localIdTrackMap[tr.youtubeID] = tr); } final sizesMap = {}; final accessTimeMap = {}; - allFiles.loop((e, _) { + allFiles.loop((e) { final path = itemToPath(e); final stats = File(path).statSync(); final accessed = stats.accessed.millisecondsSinceEpoch; @@ -158,10 +158,10 @@ class StorageCacheManager { return NamidaInkWell( animationDurationMS: 100, borderRadius: 8.0, - bgColor: Get.theme.cardTheme.color, + bgColor: namida.theme.cardTheme.color, padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( - border: enabled(sort) ? Border.all(color: Get.theme.colorScheme.primary) : null, + border: enabled(sort) ? Border.all(color: namida.theme.colorScheme.primary) : null, borderRadius: BorderRadius.circular(8.0.multipliedRadius), ), onTap: () => sortBy(sort), @@ -171,7 +171,7 @@ class StorageCacheManager { const SizedBox(width: 4.0), Text( title, - style: Get.textTheme.displayMedium, + style: namida.textTheme.displayMedium, ), const SizedBox(width: 4.0), const Icon(Broken.arrow_down_2, size: 14.0), @@ -202,12 +202,12 @@ class StorageCacheManager { /// Clear after choosing Obx( () => NamidaButton( - enabled: itemsToDeleteSize.value > 0 || itemsToDelete.isNotEmpty, - text: "${lang.DELETE.toUpperCase()} (${itemsToDeleteSize.value.fileSizeFormatted})", + enabled: itemsToDeleteSize.valueR > 0 || itemsToDelete.valueR.isNotEmpty, + text: "${lang.DELETE.toUpperCase()} (${itemsToDeleteSize.valueR.fileSizeFormatted})", onPressed: () async { final hasTemp = deleteTempFiles.value && tempFilesSizeFinal.value > 0; final finalItemsToDeleteOnlySize = itemsToDeleteSize.value - (hasTemp ? tempFilesSizeFinal.value : 0); - final firstLine = itemsToDelete.isNotEmpty || finalItemsToDeleteOnlySize > 0 ? confirmDialogText(itemsToDelete.length, finalItemsToDeleteOnlySize) : ''; + final firstLine = itemsToDelete.value.isNotEmpty || finalItemsToDeleteOnlySize > 0 ? confirmDialogText(itemsToDelete.value.length, finalItemsToDeleteOnlySize) : ''; final tempFilesLine = hasTemp ? "${lang.DELETE_TEMP_FILES} (${tempFilesSizeFinal.value.fileSizeFormatted})?" : ''; NamidaNavigator.inst.navigateDialog( dialog: CustomBlurryDialog( @@ -221,7 +221,7 @@ class StorageCacheManager { text: lang.DELETE.toUpperCase(), onPressed: () async { NamidaNavigator.inst.closeDialog(2); - onConfirm(itemsToDelete); + onConfirm(itemsToDelete.value); onDeleteTempFiles(); }, ), @@ -234,8 +234,8 @@ class StorageCacheManager { ), ], child: SizedBox( - width: Get.width, - height: Get.height * 0.65, + width: namida.width, + height: namida.height * 0.65, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -361,38 +361,45 @@ class StorageCacheManager { ), ), ), - Obx( - () => tempFilesSizeFinal.value > 0 + ObxO( + rx: tempFilesSizeFinal, + builder: (tempfs) => tempfs > 0 ? Padding( padding: const EdgeInsets.only(left: 8.0, top: 8.0, right: 8.0), - child: AnimatedOpacity( - duration: const Duration(milliseconds: 200), - opacity: deleteTempFiles.value ? 1.0 : 0.6, - child: NamidaInkWell( - borderRadius: 6.0, - bgColor: Get.theme.cardColor, - padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 6.0), - onTap: () { - deleteTempFiles.value = !deleteTempFiles.value; - if (deleteTempFiles.value) { - itemsToDeleteSize.value += tempFilesSizeFinal.value; - } else { - itemsToDeleteSize.value -= tempFilesSizeFinal.value; - } - }, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - NamidaCheckMark( - size: 12.0, - active: deleteTempFiles.value, - ), - const SizedBox(width: 8.0), - Text( - '${lang.DELETE_TEMP_FILES} (${tempFilesSizeFinal.value.fileSizeFormatted})', - style: Get.textTheme.displaySmall, - ), - ], + child: ObxO( + rx: deleteTempFiles, + builder: (deleteTempf) => AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: deleteTempFiles.value ? 1.0 : 0.6, + child: NamidaInkWell( + borderRadius: 6.0, + bgColor: namida.theme.cardColor, + padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 6.0), + onTap: () { + deleteTempFiles.toggle(); + if (deleteTempFiles.value) { + itemsToDeleteSize.value += tempFilesSizeFinal.value; + } else { + itemsToDeleteSize.value -= tempFilesSizeFinal.value; + } + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + NamidaCheckMark( + size: 12.0, + active: deleteTempf, + ), + const SizedBox(width: 8.0), + ObxO( + rx: tempFilesSizeFinal, + builder: (tempf) => Text( + '${lang.DELETE_TEMP_FILES} (${tempf.fileSizeFormatted})', + style: namida.textTheme.displaySmall, + ), + ), + ], + ), ), ), ), @@ -423,8 +430,8 @@ class _VideoTrimmer { } } - Directory(tempDir).listSyncSafe().loop((e, _) => checkFile(e)); - Directory(normalDir).listSyncSafe().loop((e, _) { + Directory(tempDir).listSyncSafe().loop((e) => checkFile(e)); + Directory(normalDir).listSyncSafe().loop((e) { if (e.path.endsWith('.download')) { checkFile(e); } @@ -436,10 +443,10 @@ class _VideoTrimmer { int size = 0; final tempDir = dirsPath['temp'] as String; final normalDir = dirsPath['normal'] as String; - Directory(tempDir).listSyncSafe().loop((e, _) { + Directory(tempDir).listSyncSafe().loop((e) { size += e.statSync().size; }); - Directory(normalDir).listSyncSafe().loop((e, _) { + Directory(normalDir).listSyncSafe().loop((e) { if (e.path.endsWith('.download')) { size += e.statSync().size; } @@ -450,14 +457,14 @@ class _VideoTrimmer { static void _deleteTempFilesIsolate(Map dirsPath) { final tempDir = dirsPath['temp'] as String; final normalDir = dirsPath['normal'] as String; - Directory(tempDir).listSyncSafe().loop((e, _) { + Directory(tempDir).listSyncSafe().loop((e) { if (e is File) { try { e.deleteSync(); } catch (_) {} } }); - Directory(normalDir).listSyncSafe().loop((e, _) { + Directory(normalDir).listSyncSafe().loop((e) { if (e.path.endsWith('.download')) { if (e is File) { try { @@ -503,7 +510,7 @@ class _AudioTrimmer { final filesMap = {}; - Directory(dirPath).listSyncSafe().loop((e, _) { + Directory(dirPath).listSyncSafe().loop((e) { if (e.path.endsWith('.part')) { if (e is File) { final filename = e.path.split(Platform.pathSeparator).last; @@ -605,7 +612,7 @@ class _Trimmer { int totalDeletedBytes = 0; int totalBytes = 0; final sizesMap = {}; - files.loop((f, _) { + files.loop((f) { final size = f.statSync().size; sizesMap[f.path] = size; totalBytes += size; diff --git a/lib/controller/tagger_controller.dart b/lib/controller/tagger_controller.dart index 62ba9ca3..de4a4911 100644 --- a/lib/controller/tagger_controller.dart +++ b/lib/controller/tagger_controller.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/services.dart'; -import 'package:get/get_rx/src/rx_types/rx_types.dart'; +import 'package:nampack/reactive/reactive.dart'; import 'package:queue/queue.dart'; import 'package:namida/class/faudiomodel.dart'; @@ -38,7 +38,7 @@ class FAudioTaggerController { late final MethodChannel _channel = const MethodChannel('faudiotagger'); bool get _defaultGroupArtworksByAlbum => settings.groupArtworksByAlbum.value; - List get _defaultAlbumIdentifier => settings.albumIdentifiers; + List get _defaultAlbumIdentifier => settings.albumIdentifiers.value; bool get _defaultKeepFileDates => settings.editTagsKeepFileDates.value; Future _readAllData({ @@ -422,7 +422,7 @@ class FAudioTaggerController { Map _getIdentifiersMap() { final map = {}; - _defaultAlbumIdentifier.loop((e, _) => map[e] = true); + _defaultAlbumIdentifier.loop((e) => map[e] = true); return map; } } diff --git a/lib/controller/thumbnail_manager.dart b/lib/controller/thumbnail_manager.dart index fb2d23fe..848868fd 100644 --- a/lib/controller/thumbnail_manager.dart +++ b/lib/controller/thumbnail_manager.dart @@ -15,7 +15,7 @@ import 'package:namida/class/http_response_wrapper.dart'; import 'package:namida/controller/ffmpeg_controller.dart'; import 'package:namida/core/constants.dart'; import 'package:namida/core/extensions.dart'; -import 'package:namida/youtube/controller/youtube_controller.dart'; +import 'package:namida/youtube/class/yt_thumbnail_wrapper.dart'; import 'package:namida/youtube/controller/youtube_history_controller.dart'; class ThumbnailManager { @@ -402,7 +402,7 @@ class _YTThumbnailDownloadManager with PortsProvider { void _onFileFinish(String itemId, File? downloadedFile, bool? notfound, bool aborted) { if (notfound != null) _notFoundThumbnails[itemId] = notfound; - _downloadCompleters[itemId].completeIfWasnt(downloadedFile); + _downloadCompleters[itemId]?.completeIfWasnt(downloadedFile); _downloadCompleters[itemId] = null; _shouldRetry[itemId] = aborted; } diff --git a/lib/controller/video_controller.dart b/lib/controller/video_controller.dart index 7237e270..0c492727 100644 --- a/lib/controller/video_controller.dart +++ b/lib/controller/video_controller.dart @@ -4,7 +4,7 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:get/get.dart' hide Response; +import 'package:namida/core/utils.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; import 'package:namida/class/media_info.dart'; @@ -136,7 +136,7 @@ class VideoController { required bool isLocal, bool? setOrientations, }) async { - final aspect = Player.inst.videoPlayerInfo?.aspectRatio; + final aspect = Player.inst.videoPlayerInfo.value?.aspectRatio; await NamidaNavigator.inst.toggleFullScreen( NamidaVideoControls( key: VideoController.inst.videoControlsKeyFullScreen, @@ -211,7 +211,7 @@ class VideoController { List getCurrentVideosInCache() { final videos = []; for (final vl in _videoCacheIDMap.values) { - vl.loop((v, _) { + vl.loop((v) { if (File(v.path).existsSync()) { videos.add(v); } @@ -231,12 +231,12 @@ class VideoController { }); } - Future updateCurrentVideo(Track track, {bool returnEarly = false}) async { + Future updateCurrentVideo(Track? track, {bool returnEarly = false}) async { isNoVideosAvailable.value = false; currentDownloadedBytes.value = null; currentVideo.value = null; currentYTQualities.clear(); - if (track == kDummyTrack) return null; + if (track == null || track == kDummyTrack) return null; if (!settings.enableVideoPlayback.value) return null; final possibleVideos = await _getPossibleVideosFromTrack(track); @@ -299,18 +299,17 @@ class VideoController { required Track track, }) async { assert(video != null || cacheIdAndPath != null); - - await _executeForCurrentTrackOnly(track, () async { - final v = cacheIdAndPath != null ? _videoCacheIDMap[cacheIdAndPath.$1]?.firstWhereEff((e) => e.path == cacheIdAndPath.$2) : video; - if (v != null) { - currentVideo.value = v; - await Player.inst.setVideo( - source: v.path, - loopingAnimation: canLoopVideo(v, track.duration), - isFile: true, - ); - } - }); + if (!_canExecuteForCurrentTrackOnly(track)) return; + + final v = cacheIdAndPath != null ? _videoCacheIDMap[cacheIdAndPath.$1]?.firstWhereEff((e) => e.path == cacheIdAndPath.$2) : video; + if (v != null) { + currentVideo.value = v; + await Player.inst.setVideo( + source: v.path, + loopingAnimation: canLoopVideo(v, track.duration), + isFile: true, + ); + } } /// loop only if video duration is less than [p] of audio. @@ -324,7 +323,7 @@ class VideoController { // only modify if not playing yt/local video, since [enableVideoPlayback] is // limited to local music. - if (Player.inst.currentQueue.isEmpty) return; + if (Player.inst.currentItem.value is! Selectable) return; if (currentValue) { // should close/hide @@ -332,7 +331,7 @@ class VideoController { YoutubeController.inst.dispose(); await Player.inst.disposeVideo(); } else { - await updateCurrentVideo(Player.inst.nowPlayingTrack); + await updateCurrentVideo(Player.inst.currentTrack?.track); } } @@ -342,31 +341,27 @@ class VideoController { _downloadTimer = null; } - FutureOr _executeForCurrentTrackOnly(Track initialTrack, FutureOr Function() execute) async { - if (initialTrack.path != Player.inst.nowPlayingTrack.path) return; - try { - await execute(); - } catch (e) { - printy(e, isError: true); - } + bool _canExecuteForCurrentTrackOnly(Track? initialTrack) { + if (initialTrack == null) return false; + final current = Player.inst.currentTrack; + if (current == null) return false; + return initialTrack.path == current.track.path; } Future fetchYTQualities(Track track) async { final available = await YoutubeController.inst.getAvailableVideoStreamsOnly(track.youtubeID); - _executeForCurrentTrackOnly(track, () { - currentYTQualities.clear(); - currentYTQualities.addAll(available); - }); + if (_canExecuteForCurrentTrackOnly(track)) currentYTQualities.assignAll(available); } Future getVideoFromYoutubeAndUpdate( String? id, { VideoStream? stream, }) async { - final tr = Player.inst.nowPlayingTrack; + final tr = Player.inst.currentTrack?.track; + if (tr == null) return null; final dv = await fetchVideoFromYoutube(id, stream: stream); if (!settings.enableVideoPlayback.value) return null; - _executeForCurrentTrackOnly(tr, () { + if (_canExecuteForCurrentTrackOnly(tr)) { currentVideo.value = dv; currentYTQualities.refresh(); if (dv != null) currentPossibleVideos.addNoDuplicates(dv); @@ -378,7 +373,7 @@ class VideoController { }, (e) => e.frameratePrecise, ); - }); + } return dv; } @@ -386,22 +381,21 @@ class VideoController { String? id, { VideoStream? stream, }) async { - if (id == null || id == '') return null; _downloadTimerCancel(); + if (id == null || id == '') return null; currentDownloadedBytes.value = null; + final initialTrack = Player.inst.currentTrack?.track; + int downloaded = 0; void updateCurrentBytes() { + if (!_canExecuteForCurrentTrackOnly(initialTrack)) return; + if (downloaded > 0) currentDownloadedBytes.value = downloaded; printy('Video Download: ${currentDownloadedBytes.value?.fileSizeFormatted}'); } - _downloadTimer = Timer.periodic(const Duration(seconds: 1), (timer) { - updateCurrentBytes(); - }); - - final initialTrack = Player.inst.nowPlayingTrack; - void updateValuesCT(void Function() execute) => _executeForCurrentTrackOnly(initialTrack, execute); + _downloadTimer = Timer.periodic(const Duration(seconds: 1), (_) => updateCurrentBytes()); final downloadedVideo = await YoutubeController.inst.downloadYoutubeVideo( canStartDownloading: () => settings.enableVideoPlayback.value, @@ -409,7 +403,7 @@ class VideoController { stream: stream, onAvailableQualities: (availableStreams) {}, onChoosingQuality: (choosenStream) { - updateValuesCT(() { + if (_canExecuteForCurrentTrackOnly(initialTrack)) { currentVideo.value = NamidaVideo( path: '', ytID: id, @@ -421,21 +415,19 @@ class VideoController { durationMS: choosenStream.durationMS ?? 0, bitrate: choosenStream.bitrate ?? 0, ); - }); + } }, onInitialFileSize: (initialFileSize) { - updateValuesCT(() { - downloaded = initialFileSize; - updateCurrentBytes(); - }); + downloaded = initialFileSize; + updateCurrentBytes(); }, downloadingStream: (downloadedBytesLength) { - updateValuesCT(() { - downloaded += downloadedBytesLength; - }); + downloaded += downloadedBytesLength; }, ); + updateCurrentBytes(); + if (downloadedVideo != null) { _videoCacheIDMap.addNoDuplicatesForce(downloadedVideo.ytID ?? '', downloadedVideo); await _saveCachedVideosFile(); @@ -559,22 +551,22 @@ class VideoController { final cacheVideosInfoFile = await file.readAsJson() as List?; final vl = cacheVideosInfoFile?.mapped((e) => NamidaVideo.fromJson(e)); _videoCacheIDMap.clear(); - vl?.loop((e, index) => _videoCacheIDMap.addForce(e.ytID ?? '', e)); + vl?.loop((e) => _videoCacheIDMap.addForce(e.ytID ?? '', e)); Future fetchCachedVideos() async { final cachedVideos = await _checkIfVideosInMapValid(_videoCacheIDMap); printy('videos cached: ${cachedVideos.length}'); _videoCacheIDMap.clear(); - cachedVideos.entries.toList().loop((videoEntry, _) { - videoEntry.value.loop((e, _) { + cachedVideos.entries.toList().loop((videoEntry) { + videoEntry.value.loop((e) { _videoCacheIDMap.addForce(videoEntry.key, e); }); }); final newCachedVideos = await _checkForNewVideosInCache(cachedVideos); printy('videos cached new: ${newCachedVideos.length}'); - newCachedVideos.entries.toList().loop((videoEntry, _) { - videoEntry.value.loop((e, _) { + newCachedVideos.entries.toList().loop((videoEntry) { + videoEntry.value.loop((e) { _videoCacheIDMap.addForce(videoEntry.key, e); }); }); @@ -588,7 +580,7 @@ class VideoController { scanLocalVideos(fillPathsOnly: true, extractIfFileNotFound: false), // this will get paths only and disables extracting whole local videos on startup ]); - if (!Player.inst.videoInitialized) await updateCurrentVideo(Player.inst.nowPlayingTrack); + if (Player.inst.videoPlayerInfo.value?.isInitialized != true) await updateCurrentVideo(Player.inst.currentTrack?.track); } Future scanLocalVideos({ @@ -621,7 +613,7 @@ class VideoController { }, ); printy('videos local: ${localVideos.length}'); - localVideos.loop((e, index) { + localVideos.loop((e) { _videoPathsMap[e.path] = e; }); resetCounters(); @@ -631,7 +623,7 @@ class VideoController { Future _saveCachedVideosFile() async { final file = File(AppPaths.VIDEOS_CACHE); final mapValuesTotal = >[]; - _videoCacheIDMap.values.toList().loop((e, index) { + _videoCacheIDMap.values.toList().loop((e) { mapValuesTotal.addAll(e.map((e) => e.toJson())); }); final resultFile = await file.writeAsJson(mapValuesTotal); @@ -668,10 +660,10 @@ class VideoController { final videosInMap = idsMap.entries.toList(); - videosInMap.loop((ve, _) { + videosInMap.loop((ve) { final id = ve.key; final vl = ve.value; - vl.loop((v, _) { + vl.loop((v) { final file = File(v.path); // --- File Exists, will be added either instantly, or by fetching new metadata. if (file.existsSync()) { @@ -855,7 +847,7 @@ class VideoController { final parameters = { 'allAvailableDirectories': allAvailableDirectories, - 'directoriesToExclude': settings.directoriesToExclude.toList(), + 'directoriesToExclude': settings.directoriesToExclude.value, 'extensions': kVideoFilesExtensions, }; diff --git a/lib/controller/waveform_controller.dart b/lib/controller/waveform_controller.dart index 7858609a..ce3591ae 100644 --- a/lib/controller/waveform_controller.dart +++ b/lib/controller/waveform_controller.dart @@ -1,4 +1,4 @@ -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:waveform_extractor/waveform_extractor.dart'; import 'package:namida/controller/settings_controller.dart'; @@ -11,7 +11,8 @@ class WaveformController { int get _defaultUserBarsCount => settings.waveformTotalBars.value; - final isWaveformUIEnabled = false.obs; + RxBaseCore get isWaveformUIEnabled => _isWaveformUIEnabled; + final _isWaveformUIEnabled = false.obso; late List currentWaveformUI = List.filled(_defaultUserBarsCount, -1, growable: false); @@ -24,7 +25,7 @@ class WaveformController { void resetWaveform() { _currentWaveform = []; _currentScaleMap = {}; - isWaveformUIEnabled.value = false; + _isWaveformUIEnabled.value = false; } /// Extracts waveform data from a given track, or immediately read from .wave file if exists, then assigns wavedata to [_currentWaveform]. @@ -72,7 +73,7 @@ class WaveformController { original: _currentWaveform, )); currentWaveformUI = waveform; - isWaveformUIEnabled.value = true; + _isWaveformUIEnabled.value = true; } static List _calculateUIWaveformIsolate(({List original, int targetSize}) params) { @@ -90,7 +91,7 @@ class WaveformController { static Map> _downscaledWaveformLists(({List original, List targetSizes}) params) { final newLists = >{}; const maxClamping = 64.0; - params.targetSizes.loop((targetSize, index) { + params.targetSizes.loop((targetSize) { newLists[targetSize] = params.original.changeListSize( targetSize: targetSize, clampToMax: maxClamping, diff --git a/lib/core/constants.dart b/lib/core/constants.dart index 6fee6369..ebd46ab2 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -1,7 +1,6 @@ // ignore_for_file: non_constant_identifier_names, constant_identifier_names import 'dart:async'; -import 'dart:collection'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/services.dart'; @@ -280,10 +279,7 @@ const k_PLAYLIST_NAME_HISTORY = '_HISTORY_'; const k_PLAYLIST_NAME_MOST_PLAYED = '_MOST_PLAYED_'; const k_PLAYLIST_NAME_AUTO_GENERATED = '_AUTO_GENERATED_'; -List get allTracksInLibrary => UnmodifiableListView(Indexer.inst.tracksInfoList); - -bool get shouldAlbumBeSquared => - (settings.albumGridCount.value > 1 && !settings.useAlbumStaggeredGridView.value) || (settings.albumGridCount.value == 1 && settings.forceSquaredAlbumThumbnail.value); +List get allTracksInLibrary => Indexer.inst.tracksInfoList.value; /// Stock Video Qualities List final List kStockVideoQualities = [ @@ -482,7 +478,7 @@ class UnknownTags { int get currentTimeMS => DateTime.now().millisecondsSinceEpoch; -const kThemeAnimationDurationMS = 350; +const kThemeAnimationDurationMS = 250; const kMaximumSleepTimerTracks = 40; const kMaximumSleepTimerMins = 180; diff --git a/lib/core/dimensions.dart b/lib/core/dimensions.dart index f6f37d8b..93482c8a 100644 --- a/lib/core/dimensions.dart +++ b/lib/core/dimensions.dart @@ -1,13 +1,15 @@ +// ignore_for_file: avoid_rx_value_getter_outside_obx import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/class/track.dart'; -import 'package:namida/controller/history_controller.dart'; import 'package:namida/controller/navigator_controller.dart'; import 'package:namida/controller/player_controller.dart'; import 'package:namida/controller/scroll_search_controller.dart'; -import 'package:namida/controller/selected_tracks_controller.dart'; import 'package:namida/controller/settings_controller.dart'; import 'package:namida/core/enums.dart'; +import 'package:namida/core/namida_converter_ext.dart'; +import 'package:namida/core/utils.dart'; +import 'package:namida/youtube/class/youtube_id.dart'; import 'package:namida/youtube/youtube_miniplayer.dart'; class Dimensions { @@ -17,44 +19,49 @@ class Dimensions { final _kMiniplayerBottomPadding = 90.0; - bool get shouldHideFAB { - final fab = settings.floatingActionButton.value; - final route = NamidaNavigator.inst.currentRoute?.route; - final shouldHide = ScrollSearchController.inst.isGlobalSearchMenuShown.value + bool get shouldHideFABR { + final fab = settings.floatingActionButton.valueR; + final currentRoute = NamidaNavigator.inst.currentRouteR; + final route = currentRoute?.route; + final shouldHide = ScrollSearchController.inst.isGlobalSearchMenuShown.valueR ? false : fab == FABType.none || route == RouteType.SETTINGS_page || // bcz no search route == RouteType.SETTINGS_subpage || // bcz no search route == RouteType.YOUTUBE_PLAYLIST_DOWNLOAD_SUBPAGE || // bcz has fab route == RouteType.SUBPAGE_INDEXER_UPDATE_MISSING_TRACKS || // bcz has fab - ((fab == FABType.shuffle || fab == FABType.play) && SelectedTracksController.inst.currentAllTracks.isEmpty) || - (settings.selectedLibraryTab.value == LibraryTab.tracks && LibraryTab.tracks.isBarVisible == false); + ((fab == FABType.shuffle || fab == FABType.play) && currentRoute?.hasTracksInside() != true) || + (settings.selectedLibraryTab.valueR == LibraryTab.tracks && LibraryTab.tracks.isBarVisible == false); return shouldHide; } /// + active miniplayer padding - double get globalBottomPaddingEffective { - return (Player.inst.currentQueueYoutube.isNotEmpty - ? settings.youtubeStyleMiniplayer.value + double get globalBottomPaddingEffectiveR { + final currentItem = Player.inst.currentItem.valueR; + return (currentItem is YoutubeID + ? settings.youtubeStyleMiniplayer.valueR ? kYoutubeMiniplayerHeight : _kMiniplayerBottomPadding - : Player.inst.currentQueue.isNotEmpty + : currentItem is Selectable ? _kMiniplayerBottomPadding : 0.0) + 12.0; } /// + floating action button padding - double get globalBottomPaddingFAB { - return shouldHideFAB ? 0.0 : kFABHeight; + double get globalBottomPaddingFABR { + return shouldHideFABR ? 0.0 : kFABHeight; } /// + active miniplayer padding /// + floating action button padding - double get globalBottomPaddingTotal { - return globalBottomPaddingFAB + globalBottomPaddingEffective; + double get globalBottomPaddingTotalR { + return globalBottomPaddingFABR + globalBottomPaddingEffectiveR; } + bool get shouldAlbumBeSquared => + (settings.albumGridCount.value > 1 && !settings.useAlbumStaggeredGridView.value) || (settings.albumGridCount.value == 1 && settings.forceSquaredAlbumThumbnail.value); + static const tileBottomMargin = 4.0; static const tileBottomMargin6 = 6.0; static const _tileAdditionalMargin = 4.0; @@ -119,7 +126,6 @@ class Dimensions { void updateTrackTileDimensions() { trackTileItemExtent = settings.trackListTileHeight.value + totalVerticalDistance; - HistoryController.inst.calculateAllItemsExtentsInHistory(); } void updateAlbumTileDimensions() { @@ -129,7 +135,7 @@ class Dimensions { (double, double, double) _getSizes(int gridCount, bool biggerFont) { final inverseGrid = 4 - gridCount; final fontSize = biggerFont ? (18.0 - (gridCount * 1.7)) : (16.0 - (gridCount * 1.8)); - final thumbnailSize = (Get.width / gridCount) - gridHorizontalPadding * 2; + final thumbnailSize = (namida.width / gridCount) - gridHorizontalPadding * 2; return (thumbnailSize, fontSize, 2.0 * inverseGrid); } @@ -140,8 +146,8 @@ class Dimensions { /// {@endtemplate} } -EdgeInsets get kBottomPaddingInsets => EdgeInsets.only(bottom: Dimensions.inst.globalBottomPaddingTotal); -SizedBox get kBottomPaddingWidget => SizedBox(height: Dimensions.inst.globalBottomPaddingTotal); +EdgeInsets get kBottomPaddingInsets => EdgeInsets.only(bottom: Dimensions.inst.globalBottomPaddingTotalR); +SizedBox get kBottomPaddingWidget => SizedBox(height: Dimensions.inst.globalBottomPaddingTotalR); SliverPadding get kBottomPaddingWidgetSliver => SliverPadding(padding: kBottomPaddingInsets); // ---- Constant Values ---- diff --git a/lib/core/extensions.dart b/lib/core/extensions.dart index 2b52570b..e61aac9f 100644 --- a/lib/core/extensions.dart +++ b/lib/core/extensions.dart @@ -24,7 +24,7 @@ import 'package:namida/ui/widgets/custom_widgets.dart'; export 'package:dart_extensions/dart_extensions.dart'; -extension TracksSelectableUtils on List { +extension TracksSelectableUtils on Iterable { String get displayTrackKeyword => length.displayTrackKeyword; List toImageTracks([int? limit = 4]) { @@ -62,15 +62,13 @@ extension TracksUtils on List { Set toUniqueAlbums() { final tracks = this; final albums = {}; - tracks.loop((t, _) => albums.add(t.albumIdentifier)); + tracks.loop((t) => albums.add(t.albumIdentifier)); return albums; } String get totalSizeFormatted { int size = 0; - loop((t, index) { - size += t.size; - }); + loop((t) => size += t.size); return size.fileSizeFormatted; } @@ -184,9 +182,9 @@ extension DisplayKeywords on int { extension YearDateFormatted on int { String get formattedTime => getTimeFormatted(hourChar: 'h', minutesChar: 'min', separator: ' '); - String get yearFormatted => getYearFormatted(settings.dateTimeFormat.value); + String get yearFormatted => getYearFormatted(settings.dateTimeFormat.value); // non reactive - String get dateFormatted => formatTimeFromMSSE(settings.dateTimeFormat.value); + String get dateFormatted => formatTimeFromMSSE(settings.dateTimeFormat.value); // non reactive String get dateFormattedOriginal { final valInSetting = settings.dateTimeFormat.value; @@ -229,12 +227,6 @@ extension BorderRadiusSetting on double { } } -extension FontScaleSetting on double { - double get multipliedFontScale { - return this * settings.fontScaleFactor.value; - } -} - extension TrackItemSubstring on TrackTileItem { String get label => convertToString; } @@ -259,6 +251,11 @@ extension FavouriteTrack on Track { bool get isFavourite { return PlaylistController.inst.favouritesPlaylist.value.tracks.firstWhereEff((element) => element.track == this) != null; } + + bool get isFavouriteR { + // ignore: avoid_rx_value_getter_outside_obx + return PlaylistController.inst.favouritesPlaylist.valueR.tracks.firstWhereEff((element) => element.track == this) != null; + } } extension PLNAME on String { @@ -279,29 +276,31 @@ extension EnumUtils on E { extension TRACKPLAYMODE on TrackPlayMode { bool get shouldBeIndex0 => this == TrackPlayMode.selectedTrack || this == TrackPlayMode.trackAlbum || this == TrackPlayMode.trackArtist || this == TrackPlayMode.trackGenre; - List getQueue(Track trackPre, {List? searchQueue}) { - List queue = []; + List generateQueue(Track trackPre, {List? searchQueue}) { final track = trackPre.toTrackExt(); - queue = switch (this) { - TrackPlayMode.selectedTrack => [trackPre], - TrackPlayMode.searchResults => - searchQueue ?? (SearchSortController.inst.trackSearchTemp.isNotEmpty ? SearchSortController.inst.trackSearchTemp : SearchSortController.inst.trackSearchList), - TrackPlayMode.trackAlbum => track.albumIdentifier.getAlbumTracks(), - TrackPlayMode.trackArtist => track.artistsList.first.getArtistTracks(), - TrackPlayMode.trackGenre => track.artistsList.first.getGenresTracks(), - }; + final queue = switch (this) { + TrackPlayMode.selectedTrack => [trackPre], + TrackPlayMode.searchResults => searchQueue ?? + (SearchSortController.inst.trackSearchTemp.value.isNotEmpty ? SearchSortController.inst.trackSearchTemp.value : SearchSortController.inst.trackSearchList.value), + TrackPlayMode.trackAlbum => track.albumIdentifier.getAlbumTracks(), + TrackPlayMode.trackArtist => track.artistsList.firstOrNull?.getArtistTracks(), + TrackPlayMode.trackGenre => track.artistsList.firstOrNull?.getGenresTracks(), + } ?? + [trackPre]; + + final newQueue = List.from(queue); if (shouldBeIndex0) { - queue.remove(trackPre); - queue.insertSafe(0, trackPre); + newQueue.remove(trackPre); + newQueue.insertSafe(0, trackPre); } - return queue; + return newQueue; } } extension ConvertPathToTrack on String { Future removeTrackThenExtract({bool onlyIfNewFileExists = true}) async { if (onlyIfNewFileExists && !await File(this).exists()) return null; - Indexer.inst.allTracksMappedByPath.remove(Track(this)); + Indexer.inst.allTracksMappedByPath.value.remove(Track(this)); return await Indexer.inst.extractTrackInfo( trackPath: this, onMinDurTrigger: () => null, @@ -516,7 +515,7 @@ int _getDirSizeIsolate(Map params) { final recursive = params['recursive'] as bool; final followLinks = params['followLinks'] as bool; int size = 0; - Directory(dirPath).listSync(recursive: recursive, followLinks: followLinks).loop((e, index) { + Directory(dirPath).listSync(recursive: recursive, followLinks: followLinks).loop((e) { size += (e is File ? File(e.path).fileSizeSync() ?? 0 : 0); }); return size; @@ -613,15 +612,13 @@ extension FileStatsUtils on FileStat { } } -extension CompleterCompleter on Completer? { - void completeIfWasnt([FutureOr? value]) async { - final c = this; - if (c?.isCompleted == false) c?.complete(value); +extension CompleterCompleter on Completer { + void completeIfWasnt([FutureOr? value]) { + if (isCompleted == false) complete(value); } - void completeErrorIfWasnt(Object error, [StackTrace? stackTrace]) async { - final c = this; - if (c?.isCompleted == false) c?.completeError(error, stackTrace); + void completeErrorIfWasnt(Object error, [StackTrace? stackTrace]) { + if (isCompleted == false) completeError(error, stackTrace); } } @@ -635,23 +632,26 @@ extension ScrollerPerf on ScrollController { required Curve curve, final double jumpitator = 800.0, }) async { - final diff = offset - this.offset; - - if (diff > jumpitator) { - // -- is now above the target, so we jump offset-jumpitator - jumpTo(offset - jumpitator); - } else if (diff < -jumpitator) { - // -- is now under the target, so we jump offset+jumpitator - jumpTo(offset + jumpitator); - } + try { + final diff = offset - this.positions.last.pixels; + + if (diff > jumpitator) { + // -- is now above the target, so we jump offset-jumpitator + jumpTo(offset - jumpitator); + } else if (diff < -jumpitator) { + // -- is now under the target, so we jump offset+jumpitator + jumpTo(offset + jumpitator); + } + } catch (_) {} + await animateTo(offset, duration: duration, curve: curve); } } -extension NavigatorUtils on BuildContext? { +extension NavigatorUtils on BuildContext { void safePop({bool rootNavigator = false}) { final context = this; - if (context != null && context.mounted) Navigator.of(context, rootNavigator: rootNavigator).pop(); + if (context.mounted) Navigator.of(context, rootNavigator: rootNavigator).pop(); } } diff --git a/lib/core/functions.dart b/lib/core/functions.dart index b712e201..c80cd104 100644 --- a/lib/core/functions.dart +++ b/lib/core/functions.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:calendar_date_picker2/calendar_date_picker2.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:history_manager/history_manager.dart'; import 'package:namida/class/folder.dart'; @@ -28,6 +27,7 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/dialogs/edit_tags_dialog.dart'; import 'package:namida/ui/pages/equalizer_page.dart'; import 'package:namida/ui/pages/subpages/album_tracks_subpage.dart'; @@ -146,12 +146,9 @@ class NamidaOnTaps { title: lang.UNDO_CHANGES, message: lang.UNDO_CHANGES_DELETED_QUEUE, displaySeconds: 3, - button: TextButton( - onPressed: () { - QueueController.inst.reAddQueue(oldQueue); - Get.closeAllSnackbars(); - }, - child: Text(lang.UNDO), + button: ( + lang.UNDO, + () async => await QueueController.inst.reAddQueue(oldQueue), ), ); } @@ -162,12 +159,9 @@ class NamidaOnTaps { title: lang.UNDO_CHANGES, message: lang.UNDO_CHANGES_DELETED_TRACK, displaySeconds: 3, - button: TextButton( - onPressed: () { - Get.closeCurrentSnackbar(); - whatDoYouWant(); - }, - child: Text(lang.UNDO), + button: ( + lang.UNDO, + whatDoYouWant, ), ); } @@ -179,8 +173,10 @@ class NamidaOnTaps { await HistoryController.inst.removeTracksFromHistory(tracksWithDates); showSnacky( whatDoYouWant: () async { - await HistoryController.inst.addTracksToHistory(tempList); + final daysToSave = HistoryController.inst.addTracksToHistoryOnly(tempList); + HistoryController.inst.updateMostPlayedPlaylist(tempList); HistoryController.inst.sortHistoryTracks(tempList.mapped((e) => e.dateAdded.toDaysSince1970())); + await HistoryController.inst.saveHistoryToStorage(daysToSave); }, ); } else { @@ -188,7 +184,7 @@ class NamidaOnTaps { if (playlist == null) return; final Map twdAndIndexes = {}; - tracksWithDates.loop((twd, index) { + tracksWithDates.loop((twd) { twdAndIndexes[twd] = playlist.tracks.indexOf(twd); }); @@ -205,27 +201,22 @@ class NamidaOnTaps { } void onSubPageTracksSortIconTap(MediaType media) { - final sorters = (settings.mediaItemsTrackSorting[media] ?? []).obs; - final defaultSorts = >{ - MediaType.album: [SortType.trackNo, SortType.year, SortType.title], - MediaType.artist: [SortType.year, SortType.title], - MediaType.genre: [SortType.year, SortType.title], - MediaType.folder: [SortType.filename], - }; + final sorters = (settings.mediaItemsTrackSorting.value[media] ?? []).obs; final allSorts = List.from(SortType.values).obs; void resortVisualItems() => allSorts.sortByReverse((e) { final active = sorters.contains(e); - return active ? sorters.length - sorters.indexOf(e) : sorters.indexOf(e); + return active ? sorters.length - sorters.value.indexOf(e) : sorters.value.indexOf(e); }); resortVisualItems(); void resortMedia() { - settings.updateMediaItemsTrackSorting(media, sorters); + settings.updateMediaItemsTrackSorting(media, sorters.value); Indexer.inst.sortMediaTracksSubLists([media]); } NamidaNavigator.inst.navigateDialog( + scale: 1.0, onDisposing: () { sorters.close(); allSorts.close(); @@ -238,6 +229,12 @@ class NamidaOnTaps { icon: const Icon(Broken.refresh), tooltip: lang.RESTORE_DEFAULTS, onPressed: () { + final defaultSorts = >{ + MediaType.album: [SortType.trackNo, SortType.year, SortType.title], + MediaType.artist: [SortType.year, SortType.title], + MediaType.genre: [SortType.year, SortType.title], + MediaType.folder: [SortType.filename], + }; final defaults = defaultSorts[media] ?? [SortType.year]; sorters.value = defaults; settings.updateMediaItemsTrackSorting(media, defaults); @@ -252,13 +249,13 @@ class NamidaOnTaps { ), ], child: SizedBox( - width: Get.width, - height: Get.height * 0.4, + width: namida.width, + height: namida.height * 0.4, child: Column( children: [ Obx( () { - final currentlyReverse = settings.mediaItemsTrackSortingReverse[media] ?? false; + final currentlyReverse = settings.mediaItemsTrackSortingReverse.valueR[media] ?? false; return ListTileWithCheckMark( title: lang.REVERSE_ORDER, active: currentlyReverse, @@ -272,9 +269,9 @@ class NamidaOnTaps { Expanded( child: Obx( () => NamidaListView( - padding: EdgeInsets.zero, + padding: const EdgeInsets.only(bottom: 12.0), itemCount: allSorts.length, - itemExtents: null, + itemExtent: null, onReorder: (oldIndex, newIndex) { if (newIndex > oldIndex) { newIndex -= 1; @@ -323,21 +320,12 @@ class NamidaOnTaps { } void openEqualizer() { - Get.to( - () => const EqualizerPage(), - id: null, - preventDuplicates: true, - transition: Transition.cupertino, - curve: Curves.easeOut, - duration: const Duration(milliseconds: 300), - opaque: false, - fullscreenDialog: false, - ); + NamidaNavigator.inst.navigateToRoot(const EqualizerPage()); } static Map _getQueuesSize(String dir) { final map = {}; - Directory(dir).listSync().loop((e, index) { + Directory(dir).listSync().loop((e) { try { if (e is File) map[int.parse(e.path.getFilenameWOExt)] = e.lengthSync(); } catch (_) {} @@ -351,7 +339,7 @@ class NamidaOnTaps { String getSubtitle(Map lookup, List datesList) { int total = 0; String? suffix; - datesList.loop((e, index) { + datesList.loop((e) { final size = lookup[e]; if (size != null) { total += size; @@ -389,17 +377,17 @@ class NamidaOnTaps { int total = 0; if (nonFavourites.value) { total += lookupNonFavourites.values.where((v) => v).length; - selectedToClear.loop((e, index) { + selectedToClear.loop((e) { total += lookup[e]?.where((element) => lookupNonFavourites[element] != true).length ?? 0; }); - selectedHomepageItemToClear.loop((e, index) { + selectedHomepageItemToClear.loop((e) { total += lookupHomepageItem[e]?.where((element) => lookupNonFavourites[element] != true).length ?? 0; }); } else { - selectedToClear.loop((e, index) { + selectedToClear.loop((e) { total += lookup[e]?.length ?? 0; }); - selectedHomepageItemToClear.loop((e, index) { + selectedHomepageItemToClear.loop((e) { total += lookupHomepageItem[e]?.length ?? 0; }); } @@ -424,15 +412,15 @@ class NamidaOnTaps { const SizedBox(width: 8.0), Obx( () => NamidaButton( - enabled: !isRemoving.value && totalToRemove.value > 0, + enabled: !isRemoving.valueR && totalToRemove.valueR > 0, textWidget: Row( mainAxisSize: MainAxisSize.min, children: [ - if (isRemoving.value) ...[ + if (isRemoving.valueR) ...[ const LoadingIndicator(), const SizedBox(width: 8.0), ], - Text("${lang.DELETE} (${totalToRemove.value})"), + Text("${lang.DELETE} (${totalToRemove.valueR})"), ], ), onPressed: () async { @@ -452,11 +440,11 @@ class NamidaOnTaps { if (nonFavourites.value) { await QueueController.inst.removeQueues(lookupNonFavourites.keys.where((v) => lookupNonFavourites[v] == true).toList()); } - for (final s in selectedToClear) { + for (final s in selectedToClear.value) { final queues = lookup[s]; if (queues != null) await QueueController.inst.removeQueues(queues); } - for (final s in selectedHomepageItemToClear) { + for (final s in selectedHomepageItemToClear.value) { final queues = lookupHomepageItem[s]; if (queues != null) await QueueController.inst.removeQueues(queues); } @@ -472,70 +460,73 @@ class NamidaOnTaps { ), ], child: SizedBox( - height: Get.height * 0.6, - width: Get.width, + height: namida.height * 0.6, + width: namida.width, child: Obx( - () => ListView( - padding: EdgeInsets.zero, - children: [ - Padding( - padding: const EdgeInsets.all(3.0), - child: ListTileWithCheckMark( - dense: true, - icon: Broken.heart_slash, - title: '${lang.NON_FAVOURITES} (${lookupNonFavourites.length})', - subtitle: getSubtitle(sizesLookupMap, lookupNonFavourites.keys.where((v) => lookupNonFavourites[v] == true).toList()), - active: nonFavourites.value, - onTap: () { - nonFavourites.value = !nonFavourites.value; - updateTotalToRemove(); + () { + final sizesLookup = sizesLookupMap.valueR; + return ListView( + padding: EdgeInsets.zero, + children: [ + Padding( + padding: const EdgeInsets.all(3.0), + child: ListTileWithCheckMark( + dense: true, + icon: Broken.heart_slash, + title: '${lang.NON_FAVOURITES} (${lookupNonFavourites.length})', + subtitle: getSubtitle(sizesLookup, lookupNonFavourites.keys.where((v) => lookupNonFavourites[v] == true).toList()), + active: nonFavourites.valueR, + onTap: () { + nonFavourites.toggle(); + updateTotalToRemove(); + }, + ), + ), + const NamidaContainerDivider(margin: EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0)), + ...values.map( + (e) { + final list = lookup[e]; + return list != null && list.isNotEmpty + ? Padding( + padding: const EdgeInsets.all(3.0), + child: ListTileWithCheckMark( + dense: true, + title: "${e.toText()} (${list.length})", + subtitle: getSubtitle(sizesLookup, list), + active: selectedToClear.contains(e), + onTap: () { + selectedToClear.addOrRemove(e); + updateTotalToRemove(); + }, + ), + ) + : const SizedBox(); }, ), - ), - const NamidaContainerDivider(margin: EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0)), - ...values.map( - (e) { - final list = lookup[e]; - return list != null && list.isNotEmpty - ? Padding( - padding: const EdgeInsets.all(3.0), - child: ListTileWithCheckMark( - dense: true, - title: "${e.toText()} (${list.length})", - subtitle: getSubtitle(sizesLookupMap, list), - active: selectedToClear.contains(e), - onTap: () { - selectedToClear.addOrRemove(e); - updateTotalToRemove(); - }, - ), - ) - : const SizedBox(); - }, - ), - const NamidaContainerDivider(margin: EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0)), - ...HomePageItems.values.map( - (e) { - final list = lookupHomepageItem[e]; - return list != null && list.isNotEmpty - ? Padding( - padding: const EdgeInsets.all(3.0), - child: ListTileWithCheckMark( - dense: true, - title: "${e.toText()} (${list.length})", - subtitle: getSubtitle(sizesLookupMap, list), - active: selectedHomepageItemToClear.contains(e), - onTap: () { - selectedHomepageItemToClear.addOrRemove(e); - updateTotalToRemove(); - }, - ), - ) - : const SizedBox(); - }, - ), - ], - ), + const NamidaContainerDivider(margin: EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0)), + ...HomePageItems.values.map( + (e) { + final list = lookupHomepageItem[e]; + return list != null && list.isNotEmpty + ? Padding( + padding: const EdgeInsets.all(3.0), + child: ListTileWithCheckMark( + dense: true, + title: "${e.toText()} (${list.length})", + subtitle: getSubtitle(sizesLookup, list), + active: selectedHomepageItemToClear.contains(e), + onTap: () { + selectedHomepageItemToClear.addOrRemove(e); + updateTotalToRemove(); + }, + ), + ) + : const SizedBox(); + }, + ), + ], + ); + }, ), ), ), @@ -593,8 +584,8 @@ Future showCalendarDialog({ dialog: CustomBlurryDialog( titleWidgetInPadding: Obx( () => Text( - '$title ${daysNumber.value == 0 ? '' : "(${daysNumber.value.displayDayKeyword})"}', - style: Get.textTheme.displayLarge, + '$title ${daysNumber.valueR == 0 ? '' : "(${daysNumber.valueR.displayDayKeyword})"}', + style: namida.textTheme.displayLarge, ), ), normalTitleStyle: true, @@ -603,7 +594,7 @@ Future showCalendarDialog({ const CancelButton(), Obx( () => NamidaButton( - enabled: canGenerate.value, + enabled: canGenerate.valueR, onPressed: () => onGenerate(dates), text: buttonText, ), @@ -612,9 +603,7 @@ Future showCalendarDialog({ child: CalendarDatePicker2( onValueChanged: (value) { final dts = value.whereType().toList(); - dates - ..clear() - ..addAll(dts); + dates.assignAll(dts); if (onChanged != null) onChanged(dts); @@ -654,8 +643,8 @@ Future showNamidaBottomSheetWithTextField({ focusNode.requestFocus(); await Future.delayed(Duration.zero); // delay bcz sometimes doesnt show - // ignore: use_build_context_synchronously await showModalBottomSheet( + // ignore: use_build_context_synchronously context: context, useRootNavigator: useRootNavigator, showDragHandle: showDragHandle, @@ -738,10 +727,6 @@ double checkIfListsSimilar(List q1, List q2, {bool fullyFunctional = fa } } -bool checkIfQueueSameAsCurrent(List queue) { - return checkIfListsSimilar(queue, Player.inst.currentQueue) == 1.0; -} - bool checkIfQueueSameAsAllTracks(List queue) { return checkIfListsSimilar(queue, allTracksInLibrary) == 1.0; } @@ -782,7 +767,7 @@ Map getFilesTypeIsolate(Map parameters) { // "thumb", "album", "albumart", etc.. are covered by the check `element.contains(filename)`. final coversNames = ["folder", "front", "cover", "thumbnail", "albumartsmall"]; - allAvailableDirectories.keys.toList().loop((d, index) { + allAvailableDirectories.keys.toList().loop((d) { final hasNoMedia = allAvailableDirectories[d] ?? false; try { for (final systemEntity in d.listSyncSafe()) { @@ -833,7 +818,9 @@ Map getFilesTypeIsolate(Map parameters) { class TracksAddOnTap { void onAddTracksTap(BuildContext context) { - final currentTrack = Player.inst.nowPlayingTrack; + final currentTrackS = Player.inst.currentItem.value; + if (currentTrackS is! Selectable) return; + final currentTrack = currentTrackS.track; showAddItemsToQueueDialog( context: context, tiles: (getAddTracksTile) { @@ -889,15 +876,15 @@ class TracksAddOnTap { // -- moods from playlists. final allAvailableMoodsPlaylists = >{}; - for (final pl in PlaylistController.inst.playlistsMap.entries) { - pl.value.moods.loop((mood, _) { + for (final pl in PlaylistController.inst.playlistsMap.value.entries) { + pl.value.moods.loop((mood) { allAvailableMoodsPlaylists.addAllNoDuplicatesForce(mood, pl.value.tracks.tracks); }); } // -- moods from tracks. final allAvailableMoodsTracks = >{}; - for (final tr in Indexer.inst.trackStatsMap.entries) { - tr.value.moods.loop((mood, _) { + for (final tr in Indexer.inst.trackStatsMap.value.entries) { + tr.value.moods.loop((mood) { allAvailableMoodsTracks.addNoDuplicatesForce(mood, tr.key); }); } @@ -905,7 +892,7 @@ class TracksAddOnTap { // -- moods from track embedded tag final library = allTracksInLibrary; for (final tr in library) { - tr.moodList.loop((mood, _) { + tr.moodList.loop((mood) { allAvailableMoodsTracks.addNoDuplicatesForce(mood, tr); }); } @@ -925,7 +912,7 @@ class TracksAddOnTap { required String title, required List moodsList, required Map> allAvailableMoods, - required List selectedList, + required RxList selectedList, }) { return [ SliverToBoxAdapter( @@ -957,7 +944,7 @@ class TracksAddOnTap { Obx( () => NamidaCheckMark( size: 12.0, - active: selectedList.contains(m), + active: selectedList.valueR.contains(m), ), ), ], @@ -986,10 +973,10 @@ class TracksAddOnTap { text: lang.GENERATE, onPressed: () { final finalTracks = []; - selectedmoodsPlaylists.loop((m, _) { + selectedmoodsPlaylists.loop((m) { finalTracks.addAll(allAvailableMoodsPlaylists[m] ?? []); }); - selectedmoodsTracks.loop((m, _) { + selectedmoodsTracks.loop((m) { finalTracks.addAll(allAvailableMoodsTracks[m] ?? []); }); Player.inst.addToQueue( @@ -1034,8 +1021,8 @@ class TracksAddOnTap { onTap: (insertionType) async { NamidaNavigator.inst.closeDialog(); - final RxInt minRating = 80.obs; - final RxInt maxRating = 100.obs; + final minRating = 80.obs; + final maxRating = 100.obs; await NamidaNavigator.inst.navigateDialog( onDisposing: () { minRating.close(); @@ -1081,7 +1068,7 @@ class TracksAddOnTap { const SizedBox(height: 2.0), Obx( () => Text( - '${minRating.value}%', + '${minRating.valueR}%', style: context.textTheme.displaySmall, ), ) @@ -1101,7 +1088,7 @@ class TracksAddOnTap { const SizedBox(height: 2.0), Obx( () => Text( - '${maxRating.value}%', + '${maxRating.valueR}%', style: context.textTheme.displaySmall, ), ), @@ -1167,7 +1154,7 @@ class TracksAddOnTap { } void onAddVideosTap(BuildContext context) { - final currentVideo = Player.inst.nowPlayingVideoID; + final currentVideo = Player.inst.currentVideo; if (currentVideo == null) return; final currentVideoId = currentVideo.id; final currentVideoName = YoutubeController.inst.getVideoName(currentVideoId) ?? currentVideoId; @@ -1179,7 +1166,7 @@ class TracksAddOnTap { return [ Obx( () { - final isLoading = NamidaYTGenerator.inst.didPrepareResources.value == false; + final isLoading = NamidaYTGenerator.inst.didPrepareResources.valueR == false; return AnimatedEnabled( enabled: !isLoading, child: getAddTracksTile( @@ -1231,7 +1218,7 @@ class TracksAddOnTap { const NamidaContainerDivider(margin: EdgeInsets.symmetric(vertical: 4.0)), Obx( () { - final isLoading = NamidaYTGenerator.inst.didPrepareResources.value == false; + final isLoading = NamidaYTGenerator.inst.didPrepareResources.valueR == false; return AnimatedEnabled( enabled: !isLoading, child: getAddTracksTile( @@ -1345,9 +1332,9 @@ class TracksAddOnTap { trailing: Obx( () => NamidaWheelSlider( totalCount: maxCount, - initValue: tracksNo.value, + initValue: tracksNo.valueR, onValueChanged: (val) => tracksNo.value = val, - text: tracksNo.value == 0 ? lang.UNLIMITED : '${tracksNo.value}', + text: tracksNo.valueR == 0 ? lang.UNLIMITED : '${tracksNo.valueR}', ), ), ), @@ -1355,7 +1342,7 @@ class TracksAddOnTap { () => CustomSwitchListTile( icon: Broken.next, title: lang.PLAY_NEXT, - value: insertN.value, + value: insertN.valueR, onChanged: (isTrue) => insertN.value = !isTrue, ), ), @@ -1370,28 +1357,26 @@ class TracksAddOnTap { () => Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(sortBy.value.toIcon(), size: 18.0), + Icon(sortBy.valueR.toIcon(), size: 18.0), const SizedBox(width: 8.0), - Text(sortBy.value.toText()), + Text(sortBy.valueR.toText()), ], ), ), itemBuilder: (context) { return >[ - ...InsertionSortingType.values - .map( - (e) => PopupMenuItem( - value: e, - child: Row( - children: [ - Icon(e.toIcon(), size: 20.0), - const SizedBox(width: 8.0), - Text(e.toText()), - ], - ), - ), - ) - .toList() + ...InsertionSortingType.values.map( + (e) => PopupMenuItem( + value: e, + child: Row( + children: [ + Icon(e.toIcon(), size: 20.0), + const SizedBox(width: 8.0), + Text(e.toText()), + ], + ), + ), + ) ]; }, onSelected: (value) => sortBy.value = value, @@ -1425,7 +1410,7 @@ class TracksAddOnTap { icon: Broken.setting_4, onPressed: () => openQueueInsertionConfigure(insertionType, title), ).animateEntrance( - showWhen: shouldShowConfigureIcon.value, + showWhen: shouldShowConfigureIcon.valueR, durationMS: 200, ), ), @@ -1440,7 +1425,7 @@ class TracksAddOnTap { NamidaIconButton( icon: Broken.setting_3, tooltip: lang.CONFIGURE, - onPressed: () => shouldShowConfigureIcon.value = !shouldShowConfigureIcon.value, + onPressed: shouldShowConfigureIcon.toggle, ), ], child: Column(children: tiles(getAddTracksTile)), diff --git a/lib/core/namida_converter_ext.dart b/lib/core/namida_converter_ext.dart index 66c4c48a..cb612c25 100644 --- a/lib/core/namida_converter_ext.dart +++ b/lib/core/namida_converter_ext.dart @@ -3,11 +3,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:history_manager/history_manager.dart'; -import 'package:namida/youtube/yt_utils.dart'; +import 'package:namida/class/folder.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; import 'package:path/path.dart' as p; +import 'package:playlist_manager/module/playlist_id.dart'; import 'package:namida/class/faudiomodel.dart'; import 'package:namida/class/lang.dart'; @@ -31,12 +31,12 @@ import 'package:namida/controller/search_sort_controller.dart'; import 'package:namida/controller/selected_tracks_controller.dart'; import 'package:namida/controller/settings_controller.dart'; import 'package:namida/core/constants.dart'; -import 'package:namida/core/dimensions.dart'; import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/functions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/dialogs/common_dialogs.dart'; import 'package:namida/ui/pages/about_page.dart'; import 'package:namida/ui/pages/albums_page.dart'; @@ -71,11 +71,7 @@ import 'package:namida/youtube/pages/yt_history_page.dart'; import 'package:namida/youtube/pages/yt_playlist_download_subpage.dart'; import 'package:namida/youtube/pages/yt_playlist_subpage.dart'; import 'package:namida/youtube/youtube_playlists_view.dart'; -import 'package:playlist_manager/module/playlist_id.dart'; - -extension LibraryTabToEnum on int { - LibraryTab toEnum() => settings.libraryTabs.elementAt(this); -} +import 'package:namida/youtube/yt_utils.dart'; extension MediaTypeUtils on MediaType { LibraryTab? toLibraryTab() { @@ -107,6 +103,8 @@ extension LibraryTabUtils on LibraryTab { return MediaType.artist; case LibraryTab.genres: return MediaType.genre; + case LibraryTab.playlists: + return MediaType.playlist; case LibraryTab.folders: return MediaType.folder; default: @@ -114,7 +112,7 @@ extension LibraryTabUtils on LibraryTab { } } - int toInt() => settings.libraryTabs.indexOf(this); + int toInt() => settings.libraryTabs.value.indexOf(this); Widget toWidget([int? gridCount, bool animateTiles = true, bool enableHero = true]) { Widget page = const SizedBox(); @@ -366,38 +364,34 @@ extension QUEUESOURCEtoTRACKS on QueueSource { void addThese(Iterable tracks) => trs.addAll(tracks.withLimit(limit)); switch (this) { case QueueSource.allTracks: - addThese(SearchSortController.inst.trackSearchList); + addThese(SearchSortController.inst.trackSearchList.value); break; case QueueSource.search: - addThese(SearchSortController.inst.trackSearchTemp); + addThese(SearchSortController.inst.trackSearchTemp.value); break; case QueueSource.mostPlayed: addThese(HistoryController.inst.currentMostPlayedTracks); break; case QueueSource.history: - dayOfHistory != null - ? addThese(HistoryController.inst.historyMap.value[dayOfHistory] ?? []) - : addThese( - HistoryController.inst.historyTracks.withLimit(limit), - ); + dayOfHistory != null ? addThese(HistoryController.inst.historyMap.value[dayOfHistory] ?? []) : addThese(HistoryController.inst.historyTracks); break; case QueueSource.favourites: addThese(PlaylistController.inst.favouritesPlaylist.value.tracks); break; case QueueSource.queuePage: - addThese(SelectedTracksController.inst.currentAllTracks); + addThese(SelectedTracksController.inst.getCurrentAllTracks()); break; case QueueSource.selectedTracks: - addThese(SelectedTracksController.inst.selectedTracks); + addThese(SelectedTracksController.inst.selectedTracks.value); break; case QueueSource.playerQueue: - addThese(Player.inst.currentQueue); + addThese(Player.inst.currentQueue.value.mapAs()); break; case QueueSource.recentlyAdded: addThese(Indexer.inst.recentlyAddedTracks); break; default: - addThese(SelectedTracksController.inst.currentAllTracks); + addThese(SelectedTracksController.inst.getCurrentAllTracks()); } return trs; @@ -567,7 +561,6 @@ extension OnYoutubeLinkOpenActionUtils on OnYoutubeLinkOpenAction { dialog: CustomBlurryDialog( title: lang.CHOOSE, normalTitleStyle: true, - contentPadding: EdgeInsets.zero, actions: [ NamidaButton( text: lang.DONE, @@ -617,7 +610,7 @@ extension OnYoutubeLinkOpenActionUtils on OnYoutubeLinkOpenAction { if (playlistNameToAddAs != '') Obx( () => CustomListTile( - enabled: !didAddToPlaylist.value, + enabled: !didAddToPlaylist.valueR, icon: Broken.add_square, title: lang.ADD_AS_A_NEW_PLAYLIST, subtitle: playlistNameToAddAs, @@ -748,40 +741,40 @@ extension WidgetsPagess on Widget { RouteType route = RouteType.UNKNOWN; switch (runtimeType) { // ----- Pages ----- - case TracksPage: + case const (TracksPage): route = RouteType.PAGE_allTracks; break; - case AlbumsPage: + case const (AlbumsPage): route = RouteType.PAGE_albums; break; - case ArtistsPage: + case const (ArtistsPage): route = RouteType.PAGE_artists; break; - case GenresPage: + case const (GenresPage): route = RouteType.PAGE_genres; break; - case PlaylistsPage: + case const (PlaylistsPage): route = RouteType.PAGE_playlists; break; - case FoldersPage: + case const (FoldersPage): route = RouteType.PAGE_folders; break; - case QueuesPage: + case const (QueuesPage): route = RouteType.PAGE_queue; break; - case AboutPage: + case const (AboutPage): route = RouteType.PAGE_about; break; // ----- Subpages ----- - case RecentlyAddedTracksPage: + case const (RecentlyAddedTracksPage): route = RouteType.SUBPAGE_recentlyAddedTracks; break; - case AlbumTracksPage: + case const (AlbumTracksPage): route = RouteType.SUBPAGE_albumTracks; name = (this as AlbumTracksPage).albumIdentifier; break; - case ArtistTracksPage: + case const (ArtistTracksPage): final page = (this as ArtistTracksPage); final type = page.type; route = type == MediaType.albumArtist @@ -791,72 +784,72 @@ extension WidgetsPagess on Widget { : RouteType.SUBPAGE_artistTracks; name = page.name; break; - case GenreTracksPage: + case const (GenreTracksPage): route = RouteType.SUBPAGE_genreTracks; name = (this as GenreTracksPage).name; break; - case NormalPlaylistTracksPage: + case const (NormalPlaylistTracksPage): route = RouteType.SUBPAGE_playlistTracks; name = (this as NormalPlaylistTracksPage).playlistName; break; - case HistoryTracksPage: + case const (HistoryTracksPage): route = RouteType.SUBPAGE_historyTracks; name = k_PLAYLIST_NAME_HISTORY; break; - case MostPlayedTracksPage: + case const (MostPlayedTracksPage): route = RouteType.SUBPAGE_mostPlayedTracks; name = k_PLAYLIST_NAME_MOST_PLAYED; break; - case QueueTracksPage: + case const (QueueTracksPage): route = RouteType.SUBPAGE_queueTracks; name = (this as QueueTracksPage).queue.date.toString(); break; - case IndexerMissingTracksSubpage: + case const (IndexerMissingTracksSubpage): route = RouteType.SUBPAGE_INDEXER_UPDATE_MISSING_TRACKS; break; // ----- Search Results ----- - case AlbumSearchResultsPage: + case const (AlbumSearchResultsPage): route = RouteType.SEARCH_albumResults; break; - case ArtistSearchResultsPage: + case const (ArtistSearchResultsPage): route = RouteType.SEARCH_artistResults; break; // ----- Settings ----- - case SettingsPage: + case const (SettingsPage): route = RouteType.SETTINGS_page; break; - case SettingsSubPage: + case const (SettingsSubPage): route = RouteType.SETTINGS_subpage; name = (this as SettingsSubPage).title; break; - case YouTubeHomeView: + case const (YouTubeHomeView): route = RouteType.YOUTUBE_HOME; break; - case YoutubePlaylistsView: + case const (YoutubePlaylistsView): route = RouteType.YOUTUBE_PLAYLISTS; break; - case YTNormalPlaylistSubpage: + case const (YTNormalPlaylistSubpage): route = RouteType.YOUTUBE_PLAYLIST_SUBPAGE; name = (this as YTNormalPlaylistSubpage).playlistName; break; - case YTHostedPlaylistSubpage: + case const (YTHostedPlaylistSubpage): route = RouteType.YOUTUBE_PLAYLIST_SUBPAGE_HOSTED; name = (this as YTHostedPlaylistSubpage).playlist.name ?? ''; break; - case YTPlaylistDownloadPage: + case const (YTPlaylistDownloadPage): route = RouteType.YOUTUBE_PLAYLIST_DOWNLOAD_SUBPAGE; name = (this as YTPlaylistDownloadPage).playlistName; break; - case YoutubeHistoryPage: + case const (YoutubeHistoryPage): route = RouteType.YOUTUBE_HISTORY_SUBPAGE; break; - case YTMostPlayedVideosPage: + case const (YTMostPlayedVideosPage): route = RouteType.YOUTUBE_MOST_PLAYED_SUBPAGE; break; - case YTLikedVideosPage: + case const (YTLikedVideosPage): route = RouteType.YOUTUBE_LIKED_SUBPAGE; break; } @@ -866,64 +859,31 @@ extension WidgetsPagess on Widget { } extension RouteUtils on NamidaRoute { - /// Mainly used for sending to [generalPopupDialog] and use these tracks to remove from playlist. - Iterable? get tracksWithDateInside { - switch (route) { - case RouteType.SUBPAGE_playlistTracks: - return PlaylistController.inst.getPlaylist(name)?.tracks; - case RouteType.SUBPAGE_historyTracks: - return HistoryController.inst.historyTracks; - - default: - null; - } - return null; + List tracksListInside() { + final iter = tracksInside(); + return iter is List ? iter as List : iter.toList(); } - List get tracksInside { - final tr = []; - switch (route) { - case RouteType.PAGE_allTracks: - tr.addAll(SearchSortController.inst.trackSearchList); - break; - case RouteType.PAGE_folders: - tr.addAll(Folders.inst.currentTracks); - break; - case RouteType.SUBPAGE_albumTracks: - tr.addAll(name.getAlbumTracks()); - break; - case RouteType.SUBPAGE_artistTracks: - tr.addAll(name.getArtistTracks()); - break; - case RouteType.SUBPAGE_albumArtistTracks: - tr.addAll(name.getAlbumArtistTracks()); - break; - case RouteType.SUBPAGE_composerTracks: - tr.addAll(name.getComposerTracks()); - break; - case RouteType.SUBPAGE_genreTracks: - tr.addAll(name.getGenresTracks()); - break; - case RouteType.SUBPAGE_queueTracks: - tr.addAll(name.getQueue()?.tracks ?? []); - break; - case RouteType.SUBPAGE_playlistTracks: - tr.addAll(PlaylistController.inst.getPlaylist(name)?.tracks ?? []); - break; - case RouteType.SUBPAGE_historyTracks: - tr.addAll(HistoryController.inst.historyTracks); - break; - case RouteType.SUBPAGE_mostPlayedTracks: - tr.addAll(HistoryController.inst.currentMostPlayedTracks); - break; - case RouteType.SUBPAGE_recentlyAddedTracks: - tr.addAll(Indexer.inst.recentlyAddedTracks); - break; - - default: - null; - } - return tr; + bool hasTracksInside() => tracksInside().isNotEmpty; + + /// NOTE: any modification done to this will be reflected in the original list. + Iterable tracksInside() { + return switch (route) { + RouteType.PAGE_allTracks => SearchSortController.inst.trackSearchList.value, + RouteType.PAGE_folders => Folders.inst.currentFolder.value?.tracks(), + RouteType.SUBPAGE_albumTracks => name.getAlbumTracks(), + RouteType.SUBPAGE_artistTracks => name.getArtistTracks(), + RouteType.SUBPAGE_albumArtistTracks => name.getAlbumArtistTracks(), + RouteType.SUBPAGE_composerTracks => name.getComposerTracks(), + RouteType.SUBPAGE_genreTracks => name.getGenresTracks(), + RouteType.SUBPAGE_queueTracks => name.getQueue()?.tracks, + RouteType.SUBPAGE_playlistTracks => PlaylistController.inst.getPlaylist(name)?.tracks, + RouteType.SUBPAGE_historyTracks => HistoryController.inst.historyTracks, + RouteType.SUBPAGE_mostPlayedTracks => HistoryController.inst.currentMostPlayedTracks, + RouteType.SUBPAGE_recentlyAddedTracks => Indexer.inst.recentlyAddedTracks, + _ => [], + } ?? + []; } /// Currently Supports only [RouteType.SUBPAGE_albumTracks], [RouteType.SUBPAGE_artistTracks], @@ -967,7 +927,10 @@ extension RouteUtils on NamidaRoute { finalWidget = getTextWidget(lang.ARTISTS); break; case RouteType.PAGE_queue: - finalWidget = Obx(() => getTextWidget("${lang.QUEUES} • ${QueueController.inst.queuesMap.value.length}")); + finalWidget = ObxO( + rx: QueueController.inst.queuesMap, + builder: (qmap) => getTextWidget("${lang.QUEUES} • ${qmap.length}"), + ); break; default: null; @@ -1131,15 +1094,13 @@ extension RouteUtils on NamidaRoute { // ---- Playlist Tracks ---- getAnimatedCrossFade( - child: Obx( - () { - final reorderable = PlaylistController.inst.canReorderTracks.value; - return NamidaAppBarIcon( - tooltip: reorderable ? lang.DISABLE_REORDERING : lang.ENABLE_REORDERING, - icon: reorderable ? Broken.forward_item : Broken.lock_1, - onPressed: () => PlaylistController.inst.canReorderTracks.value = !PlaylistController.inst.canReorderTracks.value, - ); - }, + child: ObxO( + rx: PlaylistController.inst.canReorderTracks, + builder: (reorderable) => NamidaAppBarIcon( + tooltip: reorderable ? lang.DISABLE_REORDERING : lang.ENABLE_REORDERING, + icon: reorderable ? Broken.forward_item : Broken.lock_1, + onPressed: () => PlaylistController.inst.canReorderTracks.value = !PlaylistController.inst.canReorderTracks.value, + ), ), shouldShow: route == RouteType.SUBPAGE_playlistTracks, ), @@ -1151,15 +1112,13 @@ extension RouteUtils on NamidaRoute { ), getAnimatedCrossFade( - child: Obx( - () { - final reorderable = ytplc.YoutubePlaylistController.inst.canReorderVideos.value; - return NamidaAppBarIcon( - tooltip: reorderable ? lang.DISABLE_REORDERING : lang.ENABLE_REORDERING, - icon: reorderable ? Broken.forward_item : Broken.lock_1, - onPressed: () => ytplc.YoutubePlaylistController.inst.canReorderVideos.value = !ytplc.YoutubePlaylistController.inst.canReorderVideos.value, - ); - }, + child: ObxO( + rx: ytplc.YoutubePlaylistController.inst.canReorderVideos, + builder: (reorderable) => NamidaAppBarIcon( + tooltip: reorderable ? lang.DISABLE_REORDERING : lang.ENABLE_REORDERING, + icon: reorderable ? Broken.forward_item : Broken.lock_1, + onPressed: () => ytplc.YoutubePlaylistController.inst.canReorderVideos.value = !ytplc.YoutubePlaylistController.inst.canReorderVideos.value, + ), ), shouldShow: route == RouteType.YOUTUBE_PLAYLIST_SUBPAGE, ), @@ -1224,14 +1183,10 @@ extension QueueFromMap on int { Queue? getQueue() => QueueController.inst.queuesMap.value[this]; } -extension TrackTileItemExtentExt on Iterable { - List? toTrackItemExtents() => length == 0 ? null : List.filled(length, Dimensions.inst.trackTileItemExtent); -} - extension ThemeDefaultColors on BuildContext { Color defaultIconColor([Color? mainColor, Color? secondaryColor]) => Color.alphaBlend( (mainColor ?? CurrentColor.inst.color).withAlpha(120), - secondaryColor ?? theme.colorScheme.onBackground, + secondaryColor ?? theme.colorScheme.onSurface, ); } diff --git a/lib/core/themes.dart b/lib/core/themes.dart index 088355b3..1e46b70d 100644 --- a/lib/core/themes.dart +++ b/lib/core/themes.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - import 'package:namida/controller/current_color.dart'; import 'package:namida/controller/settings_controller.dart'; import 'package:namida/core/extensions.dart'; +import 'package:namida/core/utils.dart'; class AppThemes { static AppThemes get inst => _instance; @@ -13,7 +12,7 @@ class AppThemes { ThemeData getAppTheme([Color? color, bool? light, bool lighterDialog = true]) { color ??= CurrentColor.inst.color; - light ??= Get.theme.brightness == Brightness.light; + light ??= namida.theme.brightness == Brightness.light; final shouldUseAMOLED = !light && settings.pitchBlack.value; final pitchBlack = shouldUseAMOLED ? const Color.fromARGB(255, 0, 0, 0) : null; @@ -142,52 +141,52 @@ class AppThemes { color: light ? Color.alphaBlend(cardColor.withAlpha(180), Colors.white) : Color.alphaBlend(cardColor.withAlpha(180), Colors.black), ), textTheme: TextTheme( - bodyMedium: TextStyle( - fontSize: 14.0.multipliedFontScale, + bodyMedium: const TextStyle( + fontSize: 14.0, fontWeight: FontWeight.normal, fontFamilyFallback: fontFallback, ), - bodySmall: TextStyle( - fontSize: 14.0.multipliedFontScale, + bodySmall: const TextStyle( + fontSize: 14.0, fontWeight: FontWeight.normal, fontFamilyFallback: fontFallback, ), - titleSmall: TextStyle( - fontSize: 14.0.multipliedFontScale, + titleSmall: const TextStyle( + fontSize: 14.0, fontWeight: FontWeight.w600, fontFamilyFallback: fontFallback, ), - titleLarge: TextStyle( - fontSize: 20.0.multipliedFontScale, + titleLarge: const TextStyle( + fontSize: 20.0, fontWeight: FontWeight.w600, fontFamilyFallback: fontFallback, ), displayLarge: TextStyle( fontWeight: FontWeight.w700, - fontSize: 17.0.multipliedFontScale, + fontSize: 17.0, color: light ? Colors.black.withAlpha(160) : Colors.white.withAlpha(210), fontFamilyFallback: fontFallback, ), displayMedium: TextStyle( fontWeight: FontWeight.w600, - fontSize: 15.0.multipliedFontScale, + fontSize: 15.0, color: light ? Colors.black.withAlpha(150) : Colors.white.withAlpha(180), fontFamilyFallback: fontFallback, ), displaySmall: TextStyle( fontWeight: FontWeight.w400, - fontSize: 13.0.multipliedFontScale, + fontSize: 13.0, color: light ? Colors.black.withAlpha(120) : Colors.white.withAlpha(170), fontFamilyFallback: fontFallback, ), - headlineMedium: TextStyle( + headlineMedium: const TextStyle( fontWeight: FontWeight.normal, - fontSize: 14.0.multipliedFontScale, + fontSize: 14.0, fontFamilyFallback: fontFallback, ), - headlineSmall: TextStyle( + headlineSmall: const TextStyle( fontWeight: FontWeight.normal, - fontSize: 14.0.multipliedFontScale, + fontSize: 14.0, fontFamilyFallback: fontFallback, ), ), diff --git a/lib/core/translations/language.dart b/lib/core/translations/language.dart index 65892ef1..be6d5e77 100644 --- a/lib/core/translations/language.dart +++ b/lib/core/translations/language.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/lang.dart'; import 'package:namida/controller/settings_controller.dart'; @@ -21,7 +21,7 @@ class Language extends LanguageKeys { static final Rx _currentLanguage = kDefaultLang.obs; /// Currently Selected & Set Language. - NamidaLanguage get currentLanguage => _currentLanguage.value; + RxBaseCore get currentLanguage => _currentLanguage; /// All Available Languages fetched from `'/assets/language/translations/'` static var availableLanguages = []; diff --git a/lib/core/utils.dart b/lib/core/utils.dart new file mode 100644 index 00000000..c94536a6 --- /dev/null +++ b/lib/core/utils.dart @@ -0,0 +1,7 @@ + +import 'package:nampack/core/main_utils.dart' as nmpk; + +export 'package:nampack/nampack.dart'; + + +final namida = nmpk.nampack; diff --git a/lib/main.dart b/lib/main.dart index c298b81e..5f82a99c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,13 +10,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart'; -import 'package:get/get.dart'; import 'package:jiffy/jiffy.dart'; -import 'package:namida/controller/tagger_controller.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -36,6 +35,7 @@ import 'package:namida/controller/queue_controller.dart'; import 'package:namida/controller/scroll_search_controller.dart'; import 'package:namida/controller/settings_controller.dart'; import 'package:namida/controller/storage_cache_manager.dart'; +import 'package:namida/controller/tagger_controller.dart'; import 'package:namida/controller/video_controller.dart'; import 'package:namida/core/constants.dart'; import 'package:namida/core/enums.dart'; @@ -43,9 +43,9 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/themes.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/main_page_wrapper.dart'; import 'package:namida/packages/scroll_physics_modified.dart'; -import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/video_widget.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; import 'package:namida/youtube/controller/youtube_history_controller.dart'; @@ -61,7 +61,6 @@ void main() { void mainInitialization() async { WidgetsFlutterBinding.ensureInitialized(); - Paint.enableDithering = true; // for smooth gradient effect. // -- x this makes some issues with GestureDetector // GestureBinding.instance.resamplingEnabled = true; // for 120hz displays, should make scrolling smoother. @@ -103,7 +102,7 @@ void mainInitialization() async { if (paths.isEmpty) paths.add('/storage/emulated/0'); // -- creating directories - AppDirs.values.loop((p, _) => Directory(p).createSync(recursive: true)); + AppDirs.values.loop((p) => Directory(p).createSync(recursive: true)); kStoragePaths.addAll(paths); @@ -170,7 +169,7 @@ void mainInitialization() async { await Future.wait([ SystemChrome.setPreferredOrientations(kDefaultOrientations), NamidaNavigator.inst.setDefaultSystemUI(), - FlutterDisplayMode.setHighRefreshRate(), + FlutterDisplayMode.setHighRefreshRate().catchError((_) {}), ]); NamidaNavigator.inst.setDefaultSystemUIOverlayStyle(); @@ -191,7 +190,7 @@ void mainInitialization() async { void _cleanOldLogs(Map params) { final dirPath = params['dirPath'] as String; final fileSuffix = params['fileSuffix'] as String?; - Directory(dirPath).listSync().loop((e, _) { + Directory(dirPath).listSync().loop((e) { if (e is File) { final filename = e.path.getFilename; if (filename.startsWith('logs_') && fileSuffix != null && !filename.endsWith("$fileSuffix.txt")) { @@ -220,16 +219,21 @@ void _initErrorInterpreters() { return true; }; - FlutterError.onError = (details) { - final msg = details.toDiagnosticsNode().toDescription(); - logger.error(msg, e: details.exception, st: details.stack); - }; + FlutterError.onError = kDebugMode + ? (details) { + final msg = details.toString(); + logger.error(msg, e: details.exception, st: details.stack); + } + : (details) { + final msg = details.toDiagnosticsNode().toDescription(); + logger.error(msg, e: details.exception, st: details.stack); + }; } void _initLifeCycle() { NamidaChannel.inst.addOnDestroy('main', () async { final mode = settings.player.killAfterDismissingApp.value; - if (mode == KillAppMode.always || (mode == KillAppMode.ifNotPlaying && !Player.inst.isPlaying)) { + if (mode == KillAppMode.always || (mode == KillAppMode.ifNotPlaying && !Player.inst.isPlaying.value)) { await Player.inst.pause(); await Player.inst.dispose(); } @@ -265,7 +269,7 @@ Future _initializeIntenties() async { if (files.isNotEmpty) { final paths = []; final m3uPaths = {}; - files.loop((f, _) { + files.loop((f) { final realPath = f.realPath; if (realPath != null) { final path = realPath.replaceAll('\\', ''); @@ -275,8 +279,8 @@ Future _initializeIntenties() async { paths.add(path); } } else { - f.value?.split('\n').loop((e, index) { - e.split('https://').loop((line, index) { + f.value?.split('\n').loop((e) { + e.split('https://').loop((line) { if (line.isNotEmpty) paths.add("https://$line"); }); }); @@ -374,12 +378,9 @@ Future requestIgnoreBatteryOptimizations() async { displaySeconds: 5, top: false, isError: true, - button: NamidaButton( - text: lang.DONT_ASK_AGAIN, - onPressed: () { - Get.closeCurrentSnackbar(); - settings.save(canAskForBatteryOptimizations: false); - }, + button: ( + lang.DONT_ASK_AGAIN, + () => settings.save(canAskForBatteryOptimizations: false), ), ); await Future.delayed(const Duration(seconds: 1)); @@ -387,14 +388,14 @@ Future requestIgnoreBatteryOptimizations() async { return p.isGranted; } -Future requestManageStoragePermission() async { +Future requestManageStoragePermission({bool request = true}) async { Future createDir() async => await Directory(settings.defaultBackupLocation.value).create(recursive: true); if (NamidaDeviceInfo.sdkVersion < 30) { await createDir(); return true; } - if (!await Permission.manageExternalStorage.isGranted) { + if (request && !await Permission.manageExternalStorage.isGranted) { await Permission.manageExternalStorage.request(); } @@ -427,55 +428,72 @@ class Namida extends StatelessWidget { @override Widget build(BuildContext context) { // -- no need to listen, widget is rebuilt on applifecycle - final showPipOnly = NamidaChannel.inst.isInPip.value && Player.inst.videoPlayerInfo != null; + final showPipOnly = NamidaChannel.inst.isInPip.value; return Stack( alignment: Alignment.bottomLeft, children: [ Visibility( maintainState: true, visible: !showPipOnly, - child: Obx( - () { - final codes = settings.selectedLanguage.value.code.split('_'); - return Localizations( - locale: Locale(codes.first, codes.last), - delegates: const [ - DefaultWidgetsLocalizations.delegate, - DefaultMaterialLocalizations.delegate, - ], - child: GetMaterialApp( - key: const Key('namida_app'), - debugShowCheckedModeBanner: false, - title: 'Namida', - // restorationScopeId: 'Namida', - builder: (context, widget) { - return ScrollConfiguration( - behavior: const ScrollBehaviorModified(), - child: Obx( - () { - final mode = settings.themeMode.value; - final useDarkTheme = mode == ThemeMode.dark || (mode == ThemeMode.system && MediaQuery.platformBrightnessOf(context) == Brightness.dark); - final isLight = !useDarkTheme; - final theme = AppThemes.inst.getAppTheme(CurrentColor.inst.currentColorScheme, isLight); - NamidaNavigator.inst.setSystemUIOverlayStyleCustom(isLight); - return Theme( - data: theme, - child: widget!, - ); - }, - ), - ); + child: ObxO( + rx: settings.fontScaleFactor, + builder: (fontScaleFactor) => MediaQuery( + data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(fontScaleFactor)), + child: MaterialApp( + color: kDefaultIconLightColor, + key: const Key('namida_app'), + debugShowCheckedModeBanner: false, + navigatorKey: namida.rootNavigatorKey, + title: 'Namida', + // restorationScopeId: 'Namida', + builder: (context, widget) { + // overlay entries get rebuilt on any insertion/removal, so we create app here. + final mainApp = ScrollConfiguration( + behavior: const ScrollBehaviorModified(), + child: ObxO( + rx: settings.selectedLanguage, + builder: (selectedLanguage) { + final codes = selectedLanguage.code.split('_'); + return Localizations( + locale: Locale(codes.first, codes.last), + delegates: const [ + DefaultWidgetsLocalizations.delegate, + DefaultMaterialLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + child: Obx( + () { + final mode = settings.themeMode.valueR; + final useDarkTheme = mode == ThemeMode.dark || (mode == ThemeMode.system && MediaQuery.platformBrightnessOf(context) == Brightness.dark); + final isLight = !useDarkTheme; + final theme = AppThemes.inst.getAppTheme(CurrentColor.inst.currentColorScheme, isLight); + NamidaNavigator.inst.setSystemUIOverlayStyleCustom(isLight); + return Theme( + data: theme, + child: widget!, + ); + }, + ), + ); + }, + ), + ); + return Overlay( + initialEntries: [ + OverlayEntry(builder: (_) => mainApp), + ], + ); + }, + home: MainPageWrapper( + shouldShowOnBoarding: shouldShowOnBoarding, + onContextAvailable: (ctx) { + _initialContext = ctx; + _waitForFirstBuildContext.isCompleted ? null : _waitForFirstBuildContext.complete(true); }, - home: MainPageWrapper( - shouldShowOnBoarding: shouldShowOnBoarding, - onContextAvailable: (ctx) { - _initialContext = ctx; - _waitForFirstBuildContext.isCompleted ? null : _waitForFirstBuildContext.complete(true); - }, - ), ), - ); - }, + ), + ), ), ), diff --git a/lib/main_page_wrapper.dart b/lib/main_page_wrapper.dart index 76d536d4..116b652a 100644 --- a/lib/main_page_wrapper.dart +++ b/lib/main_page_wrapper.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/controller/miniplayer_controller.dart'; import 'package:namida/controller/navigator_controller.dart'; @@ -12,6 +11,7 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/packages/miniplayer.dart'; import 'package:namida/ui/pages/main_page.dart'; import 'package:namida/ui/pages/onboarding.dart'; @@ -115,14 +115,15 @@ class NamidaDrawer extends StatelessWidget { child: Column( children: [ Expanded( - child: Obx( - () => ListView( - children: [ - const NamidaLogoContainer(), - const NamidaContainerDivider(width: 42.0, margin: EdgeInsets.all(10.0)), - ...LibraryTab.values.map( - (e) => NamidaDrawerListTile( - enabled: settings.selectedLibraryTab.value == e, + child: ListView( + children: [ + const NamidaLogoContainer(), + const NamidaContainerDivider(width: 42.0, margin: EdgeInsets.all(10.0)), + ...LibraryTab.values.map( + (e) => ObxO( + rx: settings.selectedLibraryTab, + builder: (selectedLibraryTab) => NamidaDrawerListTile( + enabled: selectedLibraryTab == e, title: e.toText(), icon: e.toIcon(), onTap: () async { @@ -132,25 +133,29 @@ class NamidaDrawer extends StatelessWidget { }, ), ), - NamidaDrawerListTile( - enabled: false, - title: lang.QUEUES, - icon: Broken.driver, - onTap: () { - NamidaNavigator.inst.navigateTo(const QueuesPage()); - toggleDrawer(); - }, - ), - ], - ), + ), + NamidaDrawerListTile( + enabled: false, + title: lang.QUEUES, + icon: Broken.driver, + onTap: () { + NamidaNavigator.inst.navigateTo(const QueuesPage()); + toggleDrawer(); + }, + ), + ], ), ), const SizedBox(height: 12.0), Material( borderRadius: BorderRadius.circular(12.0.multipliedRadius), - child: ToggleThemeModeContainer( - width: Get.width / 2.3, - blurRadius: 3.0, + child: LayoutBuilder( + builder: (context, constraints) { + return ToggleThemeModeContainer( + width: constraints.maxWidth - 12.0, + blurRadius: 3.0, + ); + }, ), ), const SizedBox(height: 8.0), @@ -161,8 +166,9 @@ class NamidaDrawer extends StatelessWidget { icon: Broken.timer_1, onTap: () { toggleDrawer(); - final minutes = Player.inst.sleepAfterMin.obs; - final tracks = Player.inst.sleepAfterTracks.obs; + final sleepConfig = Player.inst.sleepTimerConfig.value; + final minutes = sleepConfig.sleepAfterMin.obs; + final tracks = sleepConfig.sleepAfterItems.obs; NamidaNavigator.inst.navigateDialog( onDisposing: () { minutes.close(); @@ -174,31 +180,34 @@ class NamidaDrawer extends StatelessWidget { normalTitleStyle: true, actions: [ const CancelButton(), - Obx( - () => Player.inst.enableSleepAfterMins || Player.inst.enableSleepAfterTracks - ? NamidaButton( - icon: Broken.timer_pause, - text: lang.STOP, - onPressed: () { - Player.inst.resetSleepAfterTimer(); - NamidaNavigator.inst.closeDialog(); - }, - ) - : NamidaButton( - icon: Broken.timer_start, - text: lang.START, - onPressed: () { - if (minutes.value > 0 || tracks.value > 0) { - Player.inst.updateSleepTimerValues( - enableSleepAfterMins: minutes.value > 0, - enableSleepAfterTracks: tracks.value > 0, - sleepAfterMin: minutes.value, - sleepAfterTracks: tracks.value, - ); - } - NamidaNavigator.inst.closeDialog(); - }, - ), + ObxO( + rx: Player.inst.sleepTimerConfig, + builder: (currentConfig) { + return currentConfig.enableSleepAfterMins || currentConfig.enableSleepAfterItems + ? NamidaButton( + icon: Broken.timer_pause, + text: lang.STOP, + onPressed: () { + Player.inst.resetSleepAfterTimer(); + NamidaNavigator.inst.closeDialog(); + }, + ) + : NamidaButton( + icon: Broken.timer_start, + text: lang.START, + onPressed: () { + if (minutes.value > 0 || tracks.value > 0) { + Player.inst.updateSleepTimerValues( + enableSleepAfterMins: minutes.value > 0, + enableSleepAfterItems: tracks.value > 0, + sleepAfterMin: minutes.value, + sleepAfterItems: tracks.value, + ); + } + NamidaNavigator.inst.closeDialog(); + }, + ); + }, ), ], child: Column( @@ -213,10 +222,10 @@ class NamidaDrawer extends StatelessWidget { Obx( () => NamidaWheelSlider( totalCount: 180, - initValue: minutes.value, + initValue: minutes.valueR, onValueChanged: (val) => minutes.value = val, - text: "${minutes.value}m", - topText: lang.MINUTES.capitalizeFirst, + text: "${minutes.valueR}m", + topText: lang.MINUTES.capitalizeFirst(), textPadding: 8.0, ), ), @@ -225,12 +234,13 @@ class NamidaDrawer extends StatelessWidget { style: context.textTheme.displayMedium, ), // tracks - Obx( - () => NamidaWheelSlider( + ObxO( + rx: tracks, + builder: (trs) => NamidaWheelSlider( totalCount: kMaximumSleepTimerTracks, - initValue: tracks.value, + initValue: trs, onValueChanged: (val) => tracks.value = val, - text: "${tracks.value} ${lang.TRACK}", + text: "$trs ${lang.TRACK}", topText: lang.TRACKS, textPadding: 8.0, ), diff --git a/lib/packages/dots_triangle.dart b/lib/packages/dots_triangle.dart index dbe90c6f..6638e2bf 100644 --- a/lib/packages/dots_triangle.dart +++ b/lib/packages/dots_triangle.dart @@ -1,4 +1,5 @@ /// source: https://github.com/watery-desert/loading_animation_widget +library; import 'package:flutter/material.dart'; import 'dart:math' as math; @@ -7,10 +8,10 @@ class DotsTriangle extends StatefulWidget { final double size; final Color color; const DotsTriangle({ - Key? key, + super.key, required this.color, required this.size, - }) : super(key: key); + }); @override State createState() => _DotsTriangleState(); @@ -112,8 +113,9 @@ class BuildSides extends StatelessWidget { final double rotationAngle; final Offset rotationOrigin; final bool forward; + const BuildSides.forward({ - Key? key, + super.key, required this.maxLength, required this.depth, required this.color, @@ -121,11 +123,10 @@ class BuildSides extends StatelessWidget { required this.interval, this.rotationAngle = 0, this.rotationOrigin = Offset.zero, - }) : forward = true, - super(key: key); + }) : forward = true; const BuildSides.reverse({ - Key? key, + super.key, required this.maxLength, required this.depth, required this.color, @@ -133,8 +134,7 @@ class BuildSides extends StatelessWidget { required this.interval, this.rotationAngle = 0, this.rotationOrigin = Offset.zero, - }) : forward = false, - super(key: key); + }) : forward = false; @override Widget build(BuildContext context) { @@ -225,20 +225,18 @@ class RoundedRectangle extends StatelessWidget { final Color color; final bool vertical; const RoundedRectangle.vertical({ - Key? key, + super.key, required this.width, required this.height, required this.color, - }) : vertical = true, - super(key: key); + }) : vertical = true; const RoundedRectangle.horizontal({ - Key? key, + super.key, required this.width, required this.height, required this.color, - }) : vertical = false, - super(key: key); + }) : vertical = false; @override Widget build(BuildContext context) { diff --git a/lib/packages/focused_menu.dart b/lib/packages/focused_menu.dart index cb49e7ef..344d5cfc 100644 --- a/lib/packages/focused_menu.dart +++ b/lib/packages/focused_menu.dart @@ -36,7 +36,7 @@ class FocusedMenuHolder extends StatefulWidget { final Alignment menuOpenAlignment; const FocusedMenuHolder({ - Key? key, + super.key, required this.child, this.onPressed, this.menuItems = const [], @@ -61,7 +61,7 @@ class FocusedMenuHolder extends StatefulWidget { this.enableBackgroundEffects = false, this.menuHeight, this.menuOpenAlignment = Alignment.center, - }) : super(key: key); + }); @override State createState() => _FocusedMenuHolderState(); @@ -173,7 +173,7 @@ class FocusedMenuDetails extends StatelessWidget { final Alignment menuOpenAlignment; const FocusedMenuDetails({ - Key? key, + super.key, required this.menuItems, required this.child, required this.childOffset, @@ -197,7 +197,7 @@ class FocusedMenuDetails extends StatelessWidget { required this.enableBackgroundEffects, required this.menuHeightPre, required this.menuOpenAlignment, - }) : super(key: key); + }); void _onDismiss(BuildContext context) { onMenuClose?.call(); diff --git a/lib/packages/lyrics_lrc_parsed_view.dart b/lib/packages/lyrics_lrc_parsed_view.dart index 77385247..dfd3f6cc 100644 --- a/lib/packages/lyrics_lrc_parsed_view.dart +++ b/lib/packages/lyrics_lrc_parsed_view.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:lrc/lrc.dart'; +import 'package:namida/core/constants.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:namida/class/track.dart'; @@ -12,6 +12,7 @@ import 'package:namida/controller/player_controller.dart'; import 'package:namida/controller/settings_controller.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/packages/miniplayer_base.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; @@ -98,41 +99,37 @@ class LyricsLRCParsedViewState extends State { seconds: int.parse(parts[1]), milliseconds: int.parse("${parts[2]}0"), // aditional 0 to convert to millis ); - final totalDur = Player.inst.currentItemDuration ?? Player.inst.nowPlayingTrack.duration.seconds; - cal = totalDur.inMicroseconds / lyricsDuration.inMicroseconds; + final totalDurSeconds = Player.inst.currentItemDuration.value?.inSeconds ?? Player.inst.currentTrack?.track.duration ?? 0; + final totalDurMicro = totalDurSeconds * 1000 * 1000; + cal = totalDurMicro / lyricsDuration.inMicroseconds; } catch (_) {} } - timestampsMap - ..clear() - ..addEntries( - lrc.lyrics.asMap().entries.map( - (e) { - final lineTimeStamp = e.value.timestamp + Duration(milliseconds: lrc.offset ?? 0); - final calculatedForSpedUpVersions = cal == 0 ? lineTimeStamp : (lineTimeStamp * cal); - final newLrcLine = LrcLine( - timestamp: calculatedForSpedUpVersions, - lyrics: e.value.lyrics, - type: e.value.type, - args: e.value.args, - ); - return MapEntry( - calculatedForSpedUpVersions, - (e.key, newLrcLine), - ); - }, - ), - ); + timestampsMap.assignAllEntries( + lrc.lyrics.asMap().entries.map( + (e) { + final lineTimeStamp = e.value.timestamp + Duration(milliseconds: lrc.offset ?? 0); + final calculatedForSpedUpVersions = cal == 0 ? lineTimeStamp : (lineTimeStamp * cal); + final newLrcLine = LrcLine( + timestamp: calculatedForSpedUpVersions, + lyrics: e.value.lyrics, + type: e.value.type, + args: e.value.args, + ); + return MapEntry( + calculatedForSpedUpVersions, + (e.key, newLrcLine), + ); + }, + ), + ); lyrics = timestampsMap.values.map((e) => e.$2).toList(); _listenForPosition(); - _updateHighlightedLine(Player.inst.nowPlayingPosition.milliseconds, jump: true); + _updateHighlightedLine(Player.inst.nowPlayingPosition.value.milliseconds, jump: true); } - StreamSubscription? _streamSub; void _listenForPosition() { - _streamSub = Player.inst.positionStream.asBroadcastStream().listen((ms) { - _updateHighlightedLine(ms.milliseconds); - }); + Player.inst.setPositionListener((ms) => _updateHighlightedLine(ms.milliseconds)); } void _updateHighlightedLine(Duration dur, {bool forceAnimate = false, bool jump = false}) { @@ -211,7 +208,7 @@ class LyricsLRCParsedViewState extends State { void dispose() { _latestUpdatedLineIndex.close(); _latestUpdatedLine.close(); - _streamSub?.cancel(); + Player.inst.setPositionListener(null); super.dispose(); } @@ -219,7 +216,7 @@ class LyricsLRCParsedViewState extends State { Widget build(BuildContext context) { final fullscreen = widget.isFullScreenView; final initialFontSize = fullscreen ? 25.0 : 15.0; - final normalTextStyle = context.textTheme.displayMedium!.copyWith(fontSize: _fontMultiplier * initialFontSize.multipliedFontScale); + final normalTextStyle = context.textTheme.displayMedium!.copyWith(fontSize: _fontMultiplier * initialFontSize); final bottomControlsChildren = fullscreen ? [ @@ -234,7 +231,7 @@ class LyricsLRCParsedViewState extends State { tag: 'MINIPLAYER_POSITION', child: Obx( () => Text( - Player.inst.nowPlayingPosition.milliSecondsLabel, + Player.inst.nowPlayingPositionR.milliSecondsLabel, style: context.textTheme.displaySmall, ), ), @@ -247,14 +244,13 @@ class LyricsLRCParsedViewState extends State { Player.inst.previous(); }, ), - Obx( - () => NamidaIconButton( + ObxO( + rx: Player.inst.isPlaying, + builder: (isPlaying) => NamidaIconButton( horizontalPadding: 18.0, - icon: Player.inst.isPlaying ? Broken.pause : Broken.play, + icon: isPlaying ? Broken.pause : Broken.play, iconSize: 32.0, - onPressed: () { - Player.inst.togglePlayPause(); - }, + onPressed: Player.inst.togglePlayPause, ), ), NamidaIconButton( @@ -268,11 +264,15 @@ class LyricsLRCParsedViewState extends State { NamidaHero( enabled: false, tag: 'MINIPLAYER_DURATION', - child: Obx( - () => Text( - Player.inst.nowPlayingTrack.duration.secondsLabel, - style: context.textTheme.displaySmall, - ), + child: ObxO( + rx: Player.inst.currentItem, + builder: (item) { + final track = item is Selectable ? item.track : kDummyTrack; + return Text( + track.duration.secondsLabel, + style: context.textTheme.displaySmall, + ); + }, ), ), const Spacer(), @@ -330,8 +330,8 @@ class LyricsLRCParsedViewState extends State { onPointerUp: (event) { _scrollTimer = Timer(const Duration(seconds: 3), () { _canAnimateScroll = true; - if (Player.inst.isPlaying) { - _updateHighlightedLine(Player.inst.nowPlayingPosition.milliseconds, forceAnimate: true); + if (Player.inst.isPlaying.value) { + _updateHighlightedLine(Player.inst.nowPlayingPosition.value.milliseconds, forceAnimate: true); } if (_updateOpacityForEmptyLines && currentLRC != null && _checkIfTextEmpty(_currentLine)) { refreshState(() => _isCurrentLineEmpty = true); @@ -344,9 +344,9 @@ class LyricsLRCParsedViewState extends State { builder: (context) { return Obx( () { - final lrc = Lyrics.inst.currentLyricsLRC.value; + final lrc = Lyrics.inst.currentLyricsLRC.valueR; if (lrc == null) { - final text = Lyrics.inst.currentLyricsText.value; + final text = Lyrics.inst.currentLyricsText.valueR; if (!_checkIfTextEmpty(text)) { return SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24.0), @@ -367,7 +367,7 @@ class LyricsLRCParsedViewState extends State { } final color = CurrentColor.inst.miniplayerColor; - final highlighted = timestampsMap[_latestUpdatedLine.value]?.$2; + final highlighted = timestampsMap[_latestUpdatedLine.valueR]?.$2; return PageStorage( bucket: PageStorageBucket(), child: ScrollablePositionedList.builder( @@ -448,7 +448,7 @@ class LyricsLRCParsedViewState extends State { boxShadow: [ BoxShadow( blurRadius: 8.0, - color: Get.theme.scaffoldBackgroundColor.withOpacity(0.7), + color: context.theme.scaffoldBackgroundColor.withOpacity(0.7), ), ], ), diff --git a/lib/packages/lyrics_parser/parser_lrc.dart b/lib/packages/lyrics_parser/parser_lrc.dart index ed69d929..42e83a29 100644 --- a/lib/packages/lyrics_parser/parser_lrc.dart +++ b/lib/packages/lyrics_parser/parser_lrc.dart @@ -8,7 +8,7 @@ class LRCParserLrc extends LyricsParse { ///eg:[00:03.47] -> 00:03.47 RegExp valuePattern = RegExp(r"\[(\d{2}:\d{2}.\d{2,3})\]"); - LRCParserLrc(String lyric) : super(lyric); + LRCParserLrc(super.lyric); @override List parseLines({bool isMain = true}) { diff --git a/lib/packages/lyrics_parser/parser_qrc.dart b/lib/packages/lyrics_parser/parser_qrc.dart index db7e4edc..1d4369cf 100644 --- a/lib/packages/lyrics_parser/parser_qrc.dart +++ b/lib/packages/lyrics_parser/parser_qrc.dart @@ -7,7 +7,7 @@ class LRCParserQrc extends LyricsParse { RegExp advancedValuePattern = RegExp(r"\[(\d*,\d*)\]"); - LRCParserQrc(String lyric) : super(lyric); + LRCParserQrc(super.lyric); @override List parseLines({bool isMain = true}) { diff --git a/lib/packages/lyrics_parser/parser_smart.dart b/lib/packages/lyrics_parser/parser_smart.dart index 90ffac02..ba866c74 100644 --- a/lib/packages/lyrics_parser/parser_smart.dart +++ b/lib/packages/lyrics_parser/parser_smart.dart @@ -5,7 +5,7 @@ import 'parser_qrc.dart'; ///smart parser ///Parser is automatically selected class LRCParserSmart extends LyricsParse { - LRCParserSmart(String lyric) : super(lyric); + LRCParserSmart(super.lyric); @override List parseLines({bool isMain = true}) { diff --git a/lib/packages/miniplayer.dart b/lib/packages/miniplayer.dart index db52424e..8b7a2242 100644 --- a/lib/packages/miniplayer.dart +++ b/lib/packages/miniplayer.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:animated_background/animated_background.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/track.dart'; import 'package:namida/class/video.dart'; @@ -17,7 +17,6 @@ import 'package:namida/controller/scroll_search_controller.dart'; import 'package:namida/controller/settings_controller.dart'; import 'package:namida/controller/video_controller.dart'; import 'package:namida/controller/waveform_controller.dart'; -import 'package:namida/core/constants.dart'; import 'package:namida/core/dimensions.dart'; import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; @@ -28,7 +27,6 @@ import 'package:namida/core/translations/language.dart'; import 'package:namida/packages/lyrics_lrc_parsed_view.dart'; import 'package:namida/packages/miniplayer_base.dart'; import 'package:namida/ui/dialogs/common_dialogs.dart'; -import 'package:namida/ui/dialogs/set_lrc_dialog.dart'; import 'package:namida/ui/widgets/artwork.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/library/track_tile.dart'; @@ -55,6 +53,7 @@ class _MiniPlayerParentState extends State with SingleTickerPr void initState() { MiniPlayerController.inst.updateScreenValuesInitial(); MiniPlayerController.inst.initializeSAnim(this); + WidgetsBinding.instance.addPostFrameCallback((_) => setState(() {})); // workaround for empty queue view super.initState(); } @@ -85,16 +84,20 @@ class _MiniPlayerParentState extends State with SingleTickerPr ), // -- MiniPlayers - Obx( - () => Player.inst.nowPlayingVideoID != null - ? AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: settings.youtubeStyleMiniplayer.value - ? const YoutubeMiniPlayer(key: Key('ytminiplayer')) // - : const NamidaMiniPlayerYoutubeID(key: Key('actualminiplayer')), + ObxO( + rx: Player.inst.currentItem, + builder: (currentItem) => currentItem is YoutubeID + ? ObxO( + rx: settings.youtubeStyleMiniplayer, + builder: (youtubeStyleMiniplayer) => AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: youtubeStyleMiniplayer + ? const YoutubeMiniPlayer(key: Key('yt_miniplayer')) // + : const NamidaMiniPlayerYoutubeID(key: Key('local_miniplayer_yt')), + ), ) - : Player.inst.nowPlayingTrack != kDummyTrack - ? const NamidaMiniPlayerTrack(key: Key('actualminiplayer')) + : currentItem is Selectable + ? const NamidaMiniPlayerTrack(key: Key('local_miniplayer')) : const SizedBox(key: Key('empty_miniplayer')), ), ], @@ -132,7 +135,7 @@ class NamidaMiniPlayerTrack extends StatelessWidget { return MiniplayerTextData( firstLine: firstLine, secondLine: secondLine, - isLiked: track.isFavourite, + isLiked: track.isFavouriteR, onLikeTap: (isLiked) async => await PlaylistController.inst.favouriteButtonOnPressed(track), onMenuOpen: (_) => _openMenu(track), likedIcon: Broken.heart_tick, @@ -142,94 +145,90 @@ class NamidaMiniPlayerTrack extends StatelessWidget { @override Widget build(BuildContext context) { - final onSecondary = context.theme.colorScheme.onSecondaryContainer; - return Obx( - () => NamidaMiniPlayerBase( - queue: Player.inst.currentQueue, - queueItemExtent: Dimensions.inst.trackTileItemExtent, - itemBuilder: (context, i, currentIndex) { - final queue = Player.inst.currentQueue; - final track = queue[i]; - final key = Key("${i}_${track.track.path}_${queue.length}"); // queue length only for when removing current item and next is the same. - return ( - TrackTile( - key: key, - index: i, - trackOrTwd: track, - displayRightDragHandler: true, - draggableThumbnail: true, - queueSource: QueueSource.playerQueue, - cardColorOpacity: 0.5, - fadeOpacity: i < currentIndex ? 0.3 : 0.0, - onPlaying: () { - // -- to improve performance, skipping process of checking new queues, etc.. - if (i == currentIndex) { - Player.inst.togglePlayPause(); - } else { - Player.inst.skipToQueueItem(i); - } - }, - ), - key, - ); + return NamidaMiniPlayerBase( + queueItemExtent: Dimensions.inst.trackTileItemExtent, + itemBuilder: (context, i, currentIndex, queue) { + final track = queue[i] as Selectable; + final key = Key("${i}_${track.track.path}"); + return ( + TrackTile( + key: key, + index: i, + trackOrTwd: track, + displayRightDragHandler: true, + draggableThumbnail: true, + queueSource: QueueSource.playerQueue, + cardColorOpacity: 0.5, + fadeOpacity: i < currentIndex ? 0.3 : 0.0, + onPlaying: () { + // -- to improve performance, skipping process of checking new queues, etc.. + if (i == currentIndex) { + Player.inst.togglePlayPause(); + } else { + Player.inst.skipToQueueItem(i); + } + }, + ), + key, + ); + }, + getDurationMS: (currentItem) => currentItem.track.duration * 1000, + itemsKeyword: (number) => number.displayTrackKeyword, + onAddItemsTap: (currentItem) => TracksAddOnTap().onAddTracksTap(context), + topText: (currentItem) => currentItem.track.album, + onTopTextTap: (currentItem) => NamidaOnTaps.inst.onAlbumTap(currentItem.track.albumIdentifier), + onMenuOpen: (currentItem, _) => _openMenu(currentItem.track), + focusedMenuOptions: FocusedMenuOptions( + onOpen: (currentItem) { + if (settings.enableVideoPlayback.value) return true; + + ScrollSearchController.inst.unfocusKeyboard(); + NamidaNavigator.inst.navigateDialog(dialog: const Dialog(child: PlaybackSettings(isInDialog: true))); + return false; }, - getDurationMS: (currentItem) => currentItem.track.duration * 1000, - itemsKeyword: (number) => number.displayTrackKeyword, - onAddItemsTap: (currentItem) => TracksAddOnTap().onAddTracksTap(context), - topText: (currentItem) => currentItem.track.album, - onTopTextTap: (currentItem) => NamidaOnTaps.inst.onAlbumTap(currentItem.track.albumIdentifier), - onMenuOpen: (currentItem, _) => _openMenu(currentItem.track), - focusedMenuOptions: FocusedMenuOptions( - onOpen: (currentItem) { - if (settings.enableVideoPlayback.value) return true; - - ScrollSearchController.inst.unfocusKeyboard(); - NamidaNavigator.inst.navigateDialog(dialog: const Dialog(child: PlaybackSettings(isInDialog: true))); - return false; - }, - onPressed: (currentItem) => VideoController.inst.toggleVideoPlayback(), - videoIconBuilder: (currentItem, size, color) => Obx( - () => Icon( - settings.enableVideoPlayback.value ? Broken.video : Broken.headphone, - size: size, - color: color, - ), + onPressed: (currentItem) => VideoController.inst.toggleVideoPlayback(), + videoIconBuilder: (currentItem, size, color) => Obx( + () => Icon( + settings.enableVideoPlayback.valueR ? Broken.video : Broken.headphone, + size: size, + color: color, ), - builder: (currentItem) => Obx(() { - final currentVideo = VideoController.inst.currentVideo.value; - final downloadedBytes = VideoController.inst.currentDownloadedBytes.value; + ), + builder: (currentItem) { + final onSecondary = context.theme.colorScheme.onSecondaryContainer; + return Obx(() { + final currentVideo = VideoController.inst.currentVideo.valueR; + final downloadedBytes = VideoController.inst.currentDownloadedBytes.valueR; final videoTotalSize = currentVideo?.sizeInBytes ?? 0; final videoQuality = currentVideo?.resolution ?? 0; final videoFramerate = currentVideo?.framerateText(30); - final markText = VideoController.inst.isNoVideosAvailable.value ? 'x' : '?'; + final markText = VideoController.inst.isNoVideosAvailable.valueR ? 'x' : '?'; final fallbackQualityLabel = currentVideo?.nameInCache?.split('_').last; final qualityText = videoQuality == 0 ? fallbackQualityLabel ?? markText : '${videoQuality}p'; final framerateText = videoFramerate ?? ''; - return !settings.enableVideoPlayback.value - ? RichText( - maxLines: 2, - overflow: TextOverflow.ellipsis, - text: TextSpan( + return !settings.enableVideoPlayback.valueR + ? Text.rich( + TextSpan( text: lang.AUDIO, - style: context.textTheme.labelLarge?.copyWith(color: context.theme.colorScheme.onSecondaryContainer), + style: context.textTheme.labelLarge?.copyWith(fontSize: 15.0, color: context.theme.colorScheme.onSecondaryContainer), children: [ - if (settings.displayAudioInfoMiniplayer.value) + if (settings.displayAudioInfoMiniplayer.valueR) TextSpan( text: " • ${currentItem.track.audioInfoFormattedCompact}", - style: TextStyle(color: context.theme.colorScheme.primary, fontSize: 10.0.multipliedFontScale), + style: TextStyle(color: context.theme.colorScheme.primary, fontSize: 11.0), ) ], ), - ) - : RichText( maxLines: 2, overflow: TextOverflow.ellipsis, - text: TextSpan( + ) + : Text.rich( + TextSpan( text: lang.VIDEO, - style: context.textTheme.labelLarge?.copyWith(color: context.theme.colorScheme.onSecondaryContainer), + style: context.textTheme.labelLarge?.copyWith(fontSize: 15.0, color: context.theme.colorScheme.onSecondaryContainer), children: [ - if (qualityText == '?' && !ConnectivityController.inst.hasConnection) ...[ - TextSpan(text: " • ", style: TextStyle(color: onSecondary, fontSize: 13.0.multipliedFontScale)), + if (qualityText == '?' && !ConnectivityController.inst.hasConnectionR) ...[ + TextSpan(text: " • ", style: TextStyle(color: onSecondary, fontSize: 15.0)), WidgetSpan( child: Icon( Broken.global_refresh, @@ -242,88 +241,51 @@ class NamidaMiniPlayerTrack extends StatelessWidget { text: " • $qualityText$framerateText", style: TextStyle( color: context.theme.colorScheme.primary, - fontSize: 13.0.multipliedFontScale, + fontSize: 13.0, ), ), // -- if (videoTotalSize > 0) ...[ - TextSpan(text: " • ", style: TextStyle(color: context.theme.colorScheme.primary, fontSize: 13.0.multipliedFontScale)), + TextSpan(text: " • ", style: TextStyle(color: context.theme.colorScheme.primary, fontSize: 14.0)), TextSpan( text: downloadedBytes == null ? videoTotalSize.fileSizeFormatted : "${downloadedBytes.fileSizeFormatted}/${videoTotalSize.fileSizeFormatted}", - style: TextStyle(color: onSecondary, fontSize: 10.0.multipliedFontScale), + style: TextStyle(color: onSecondary, fontSize: 10.0), ), ], ], ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ); - }), - currentId: (item) => item.track.youtubeID, - loadQualities: (item) async => await VideoController.inst.fetchYTQualities(item.track), - localVideos: VideoController.inst.currentPossibleVideos, - streamVideos: VideoController.inst.currentYTQualities, - onLocalVideoTap: (item, video) async { - VideoController.inst.playVideoCurrent(video: video, track: item.track); - }, - onStreamVideoTap: (item, videoId, stream, cacheFile) async { - final cacheExists = cacheFile != null; - if (!cacheExists) await VideoController.inst.getVideoFromYoutubeAndUpdate(videoId, stream: stream); - VideoController.inst.playVideoCurrent( - video: null, - cacheIdAndPath: (videoId ?? '', cacheFile?.path ?? ''), - track: item.track, - ); - }, - ), - extraActionButton: (twd) { - final track = twd.track; - return LongPressDetector( - onLongPress: () { - showLRCSetDialog(track, CurrentColor.inst.miniplayerColor); - }, - child: IconButton( - visualDensity: VisualDensity.compact, - style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), - padding: const EdgeInsets.all(2.0), - onPressed: () { - settings.save(enableLyrics: !settings.enableLyrics.value); - Lyrics.inst.updateLyrics(track); - }, - icon: Obx( - () => settings.enableLyrics.value - ? Lyrics.inst.currentLyricsText.value == '' && Lyrics.inst.currentLyricsLRC.value == null - ? StackedIcon( - baseIcon: Broken.document, - secondaryText: !Lyrics.inst.lyricsCanBeAvailable.value ? 'x' : '?', - iconSize: 20.0, - blurRadius: 6.0, - baseIconColor: context.theme.colorScheme.onSecondaryContainer, - secondaryIconColor: context.theme.colorScheme.onSecondaryContainer, - ) - : Icon( - Broken.document, - size: 20.0, - color: context.theme.colorScheme.onSecondaryContainer, - ) - : Icon( - Broken.card_slash, - size: 20.0, - color: context.theme.colorScheme.onSecondaryContainer, - ), - ), - ), + }); + }, + currentId: (item) => item.track.youtubeID, + loadQualities: (item) async => await VideoController.inst.fetchYTQualities(item.track), + localVideos: VideoController.inst.currentPossibleVideos, + streamVideos: VideoController.inst.currentYTQualities, + onLocalVideoTap: (item, video) async { + VideoController.inst.playVideoCurrent(video: video, track: item.track); + }, + onStreamVideoTap: (item, videoId, stream, cacheFile) async { + final cacheExists = cacheFile != null; + if (!cacheExists) await VideoController.inst.getVideoFromYoutubeAndUpdate(videoId, stream: stream); + VideoController.inst.playVideoCurrent( + video: null, + cacheIdAndPath: (videoId ?? '', cacheFile?.path ?? ''), + track: item.track, ); }, - imageBuilder: (item, cp) => _TrackImage( - track: item.track, - cp: cp, - ), - currentImageBuilder: (item, bcp) => _AnimatingTrackImage( - track: item.track, - cp: bcp, - ), - textBuilder: _textBuilder, - canShowBuffering: false, ), + imageBuilder: (item, cp) => _TrackImage( + track: item.track, + cp: cp, + ), + currentImageBuilder: (item, bcp) => _AnimatingTrackImage( + track: item.track, + cp: bcp, + ), + textBuilder: _textBuilder, + canShowBuffering: false, ); } } @@ -342,14 +304,13 @@ class NamidaMiniPlayerYoutubeID extends StatelessWidget { idsNamesLookup: {video.id: info?.name}, playlistName: '', videoYTID: video, + copyUrl: true, ), ).convertItems(context); NamidaNavigator.inst.showMenu( - showMenu( - context: context, - position: RelativeRect.fromLTRB(details.globalPosition.dx, details.globalPosition.dy, 0, 0), - items: popUpItems, - ), + context: context, + position: RelativeRect.fromLTRB(details.globalPosition.dx, details.globalPosition.dy, 0, 0), + items: popUpItems, ); } @@ -377,70 +338,69 @@ class NamidaMiniPlayerYoutubeID extends StatelessWidget { @override Widget build(BuildContext context) { - final onSecondary = context.theme.colorScheme.onSecondaryContainer; - return Obx( - () => NamidaMiniPlayerBase( - queue: Player.inst.currentQueueYoutube, - queueItemExtent: Dimensions.youtubeCardItemExtent, - itemBuilder: (context, i, currentIndex) { - final queue = Player.inst.currentQueueYoutube; - final video = queue[i]; - final key = Key("${i}_${video.id}_${queue.length}"); // queue length only for when removing current item and next is the same. - return ( - YTHistoryVideoCard( - key: key, - videos: Player.inst.currentQueueYoutube, - index: i, - day: null, - playlistID: null, - playlistName: '', - openMenuOnLongPress: false, - displayTimeAgo: false, - thumbnailHeight: Dimensions.youtubeThumbnailHeight, - fromPlayerQueue: true, - draggingEnabled: true, - draggableThumbnail: true, - showMoreIcon: true, - cardColorOpacity: 0.5, - fadeOpacity: i < currentIndex ? 0.3 : 0.0, - ), - key, - ); - }, - getDurationMS: null, - itemsKeyword: (number) => number.displayVideoKeyword, - onAddItemsTap: (currentItem) => TracksAddOnTap().onAddVideosTap(context), - topText: (currentItem) => - YoutubeController.inst.currentYoutubeMetadataChannel.value?.name ?? - Player.inst.currentChannelInfo?.name ?? - YoutubeController.inst.getVideoChannelName(currentItem.id) ?? - '', - onTopTextTap: (currentItem) { - final channel = YoutubeController.inst.currentYoutubeMetadataChannel.value ?? Player.inst.currentChannelInfo; - final chid = channel?.id; - if (chid != null) NamidaNavigator.inst.navigateTo(YTChannelSubpage(channelID: chid, channel: channel)); - }, - onMenuOpen: (currentItem, d) => _openMenu(context, currentItem, d), - focusedMenuOptions: FocusedMenuOptions( - onOpen: (currentItem) => true, - onPressed: (currentItem) => Player.inst.setAudioOnlyPlayback(!Player.inst.isAudioOnlyPlayback), - videoIconBuilder: (currentItem, size, color) => Obx( - () => Icon( - !Player.inst.isAudioOnlyPlayback ? Broken.video : Broken.headphone, - size: size, - color: color, - ), + return NamidaMiniPlayerBase( + queueItemExtent: Dimensions.youtubeCardItemExtent, + itemBuilder: (context, i, currentIndex, queue) { + final video = queue[i] as YoutubeID; + final key = Key("${i}_${video.id}"); + return ( + YTHistoryVideoCard( + key: key, + videos: queue, + index: i, + day: null, + playlistID: null, + playlistName: '', + openMenuOnLongPress: false, + displayTimeAgo: false, + thumbnailHeight: Dimensions.youtubeThumbnailHeight, + fromPlayerQueue: true, + draggingEnabled: true, + draggableThumbnail: true, + showMoreIcon: true, + cardColorOpacity: 0.5, + fadeOpacity: i < currentIndex ? 0.3 : 0.0, ), - builder: (currentItem) => Obx(() { - if (Player.inst.isAudioOnlyPlayback) { + key, + ); + }, + getDurationMS: null, + itemsKeyword: (number) => number.displayVideoKeyword, + onAddItemsTap: (currentItem) => TracksAddOnTap().onAddVideosTap(context), + topText: (currentItem) => + YoutubeController.inst.currentYoutubeMetadataChannel.value?.name ?? + Player.inst.currentChannelInfo.value?.name ?? + YoutubeController.inst.getVideoChannelName(currentItem.id) ?? + '', + onTopTextTap: (currentItem) { + final channel = YoutubeController.inst.currentYoutubeMetadataChannel.value ?? Player.inst.currentChannelInfo.value; + final chid = channel?.id; + if (chid != null) NamidaNavigator.inst.navigateTo(YTChannelSubpage(channelID: chid, channel: channel)); + }, + onMenuOpen: (currentItem, d) => _openMenu(context, currentItem, d), + focusedMenuOptions: FocusedMenuOptions( + onOpen: (currentItem) => true, + onPressed: (currentItem) => Player.inst.setAudioOnlyPlayback(!settings.ytIsAudioOnlyMode.value), + videoIconBuilder: (currentItem, size, color) => Obx( + () => Icon( + !settings.ytIsAudioOnlyMode.valueR ? Broken.video : Broken.headphone, + size: size, + color: color, + ), + ), + builder: (currentItem) { + final onSecondary = context.theme.colorScheme.onSecondaryContainer; + return Obx(() { + if (settings.ytIsAudioOnlyMode.valueR) { List? textChildren; - if (settings.displayAudioInfoMiniplayer.value) { - final formatName = Player.inst.currentAudioStream?.formatName; - final bitrate = Player.inst.currentAudioStream?.bitrate ?? Player.inst.currentCachedAudio?.bitrate; + if (settings.displayAudioInfoMiniplayer.valueR) { + final audioStream = Player.inst.currentAudioStream.valueR; + final formatName = audioStream?.formatName; + final bitrate = audioStream?.bitrate ?? Player.inst.currentCachedAudio.valueR?.bitrate; final bitrateText = bitrate == null ? null : "${bitrate ~/ 1000} kps"; - final sampleRate = Player.inst.currentAudioStream?.samplerate; + final sampleRate = audioStream?.samplerate; final sampleRateText = sampleRate == null ? null : "$sampleRate khz"; - final language = Player.inst.currentAudioStream?.language ?? Player.inst.currentCachedAudio?.langaugeCode; + final language = audioStream?.language ?? Player.inst.currentCachedAudio.valueR?.langaugeCode; final finalText = [ formatName, @@ -453,38 +413,36 @@ class NamidaMiniPlayerYoutubeID extends StatelessWidget { textChildren = [ TextSpan( text: " • ${finalText.joinText(separator: ' • ')}", - style: TextStyle(color: context.theme.colorScheme.primary, fontSize: 10.0.multipliedFontScale), + style: TextStyle(color: context.theme.colorScheme.primary, fontSize: 11.0), ), ]; } } - return RichText( - maxLines: 2, - overflow: TextOverflow.ellipsis, - text: TextSpan( + return Text.rich( + TextSpan( text: lang.AUDIO, - style: context.textTheme.labelLarge?.copyWith(color: context.theme.colorScheme.onSecondaryContainer), + style: context.textTheme.labelLarge?.copyWith(fontSize: 15.0, color: context.theme.colorScheme.onSecondaryContainer), children: textChildren, ), + overflow: TextOverflow.ellipsis, + maxLines: 2, ); } else { - final stream = Player.inst.currentVideoStream; - final cached = Player.inst.currentCachedVideo; + final stream = Player.inst.currentVideoStream.valueR; + final cached = Player.inst.currentCachedVideo.valueR; int? size = stream?.sizeInBytes; if (size == null || size == 0) { size = cached?.sizeInBytes; } final sizeFinal = size ?? 0; final qualityText = stream?.resolution ?? (cached == null ? null : "${cached.resolution}p${cached.framerateText()}"); - return RichText( - maxLines: 2, - overflow: TextOverflow.ellipsis, - text: TextSpan( + return Text.rich( + TextSpan( text: lang.VIDEO, - style: context.textTheme.labelLarge?.copyWith(color: context.theme.colorScheme.onSecondaryContainer), + style: context.textTheme.labelLarge?.copyWith(fontSize: 15.0, color: context.theme.colorScheme.onSecondaryContainer), children: [ - if (stream == null && cached == null && !ConnectivityController.inst.hasConnection) ...[ - TextSpan(text: " • ", style: TextStyle(color: onSecondary, fontSize: 13.0.multipliedFontScale)), + if (stream == null && cached == null && !ConnectivityController.inst.hasConnectionR) ...[ + TextSpan(text: " • ", style: TextStyle(color: onSecondary, fontSize: 15.0)), WidgetSpan( child: Icon( Broken.global_refresh, @@ -497,69 +455,57 @@ class NamidaMiniPlayerYoutubeID extends StatelessWidget { text: " • ${qualityText ?? '?'}", style: TextStyle( color: context.theme.colorScheme.primary, - fontSize: 13.0.multipliedFontScale, + fontSize: 13.0, ), ), // -- if (sizeFinal > 0) ...[ - TextSpan(text: " • ", style: TextStyle(color: context.theme.colorScheme.primary, fontSize: 13.0.multipliedFontScale)), + TextSpan(text: " • ", style: TextStyle(color: context.theme.colorScheme.primary, fontSize: 14.0)), TextSpan( text: sizeFinal.fileSizeFormatted, - style: TextStyle(color: onSecondary, fontSize: 10.0.multipliedFontScale), + style: TextStyle(color: onSecondary, fontSize: 10.0), ), ], ], ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ); } - }), - currentId: (item) => item.id, - loadQualities: null, - localVideos: YoutubeController.inst.currentCachedQualities, - streamVideos: YoutubeController.inst.currentYTQualities, - onLocalVideoTap: (item, video) async { - 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( - stream: stream, - cachedFile: null, - useCache: true, - videoId: item.id, - ); - }, - ), - extraActionButton: (video) { - return IconButton( - tooltip: lang.COPY, - visualDensity: VisualDensity.compact, - style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), - padding: const EdgeInsets.all(2.0), - onPressed: () => YTUtils().copyVideoUrl(video.id), - icon: Icon( - Broken.copy, - size: 19.0, - color: context.theme.colorScheme.onSecondaryContainer, - ), + }); + }, + currentId: (item) => item.id, + loadQualities: null, + localVideos: YoutubeController.inst.currentCachedQualities, + streamVideos: YoutubeController.inst.currentYTQualities, + onLocalVideoTap: (item, video) async { + Player.inst.onItemPlayYoutubeIDSetQuality( + stream: null, + cachedFile: File(video.path), + videoItem: video, + useCache: true, + videoId: Player.inst.currentVideo?.id ?? '', ); }, - imageBuilder: (item, cp) => _YoutubeIDImage( - video: item, - cp: cp, - ), - currentImageBuilder: (item, bcp) => _AnimatingYoutubeIDImage( - video: item, - cp: bcp, - ), - textBuilder: (item) => _textBuilder(context, item), - canShowBuffering: true, + onStreamVideoTap: (item, videoId, stream, cacheFile) async { + Player.inst.onItemPlayYoutubeIDSetQuality( + stream: stream, + cachedFile: null, + useCache: true, + videoId: item.id, + ); + }, + ), + imageBuilder: (item, cp) => _YoutubeIDImage( + video: item, + cp: cp, ), + currentImageBuilder: (item, bcp) => _AnimatingYoutubeIDImage( + video: item, + cp: bcp, + ), + textBuilder: (item) => _textBuilder(context, item), + canShowBuffering: true, ); } } @@ -579,104 +525,140 @@ class _AnimatingTrackImage extends StatelessWidget { @override Widget build(BuildContext context) { - return Obx( - () { - final artworkGestureDoubleTapLRC = settings.artworkGestureDoubleTapLRC.value; - return DoubleTapDetector( - // -- only when lrc view is not visible, to prevent other gestures delaying. - onDoubleTap: artworkGestureDoubleTapLRC && Lyrics.inst.currentLyricsLRC.value == null - ? () { - settings.save(enableLyrics: !settings.enableLyrics.value); - Lyrics.inst.updateLyrics(track); - } - : null, - child: GestureDetector( - onLongPress: () { - Lyrics.inst.lrcViewKey?.currentState?.enterFullScreen(); - }, - onScaleStart: (details) { - final lrcState = Lyrics.inst.lrcViewKey?.currentState; - final lrcVisible = lrcState != null; - _isScalingLRC = lrcVisible; - _previousScale = lrcVisible ? 1.0 : settings.animatingThumbnailScaleMultiplier.value; - }, - onScaleUpdate: (details) { - if (_isScalingLRC || settings.artworkGestureScale.value) { - final m = (details.scale * _previousScale); - if (_isScalingLRC) { - _lrcAdditionalScale.value = m; - } else { - settings.save(animatingThumbnailScaleMultiplier: m.clamp(0.4, 1.5)); - } + return ObxO( + rx: settings.artworkGestureDoubleTapLRC, + builder: (artworkGestureDoubleTapLRC) => DoubleTapDetector( + // -- only when lrc view is not visible, to prevent other gestures delaying. + onDoubleTap: artworkGestureDoubleTapLRC && Lyrics.inst.currentLyricsLRC.value == null + ? () { + settings.save(enableLyrics: !settings.enableLyrics.value); + Lyrics.inst.updateLyrics(track); } - }, - onScaleEnd: (details) { - final lrcState = Lyrics.inst.lrcViewKey?.currentState; - if (lrcState != null) { - final pps = details.velocity.pixelsPerSecond; - if (pps.dx > 0 || pps.dy > 0) { - lrcState.enterFullScreen(); - } + : null, + child: GestureDetector( + onLongPress: () { + Lyrics.inst.lrcViewKey?.currentState?.enterFullScreen(); + }, + onScaleStart: (details) { + final lrcState = Lyrics.inst.lrcViewKey?.currentState; + final lrcVisible = lrcState != null; + _isScalingLRC = lrcVisible; + _previousScale = lrcVisible ? 1.0 : settings.animatingThumbnailScaleMultiplier.value; + }, + onScaleUpdate: (details) { + if (_isScalingLRC || settings.artworkGestureScale.value) { + final m = (details.scale * _previousScale); + if (_isScalingLRC) { + _lrcAdditionalScale.value = m; + } else { + settings.save(animatingThumbnailScaleMultiplier: m.clamp(0.4, 1.5)); } - _lrcAdditionalScale.value = 0.0; - }, - child: Obx( - () { - final videoInfo = Player.inst.videoPlayerInfo; - return Obx( - () { - final additionalScaleVideo = 0.02 * VideoController.inst.videoZoomAdditionalScale.value; - final additionalScaleLRC = 0.02 * _lrcAdditionalScale.value; - final finalScale = additionalScaleLRC + additionalScaleVideo + WaveformController.inst.getCurrentAnimatingScale(Player.inst.nowPlayingPosition); - final isInversed = settings.animatingThumbnailInversed.value; - final userScaleMultiplier = settings.animatingThumbnailScaleMultiplier.value; - return AnimatedScale( - duration: const Duration(milliseconds: 100), - scale: (isInversed ? 1.22 - finalScale : 1.13 + finalScale) * userScaleMultiplier, - child: Stack( - alignment: Alignment.center, - children: [ - videoInfo != null && videoInfo.isInitialized - ? BorderRadiusClip( - borderRadius: BorderRadius.circular((6.0 + 10.0 * cp).multipliedRadius), - child: DoubleTapDetector( - onDoubleTap: () => VideoController.inst.toggleFullScreenVideoView(isLocal: true), - child: NamidaAspectRatio( - aspectRatio: videoInfo.aspectRatio, - child: Texture(textureId: videoInfo.textureId), - ), - ), - ) - : _TrackImage( - track: track, - cp: cp, - ), - Obx( - () => AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: settings.enableLyrics.value && (Lyrics.inst.currentLyricsLRC.value != null || Lyrics.inst.currentLyricsText.value != '') - ? LyricsLRCParsedView( - key: Lyrics.inst.lrcViewKey, - cp: cp, - initialLrc: Lyrics.inst.currentLyricsLRC.value, - videoOrImage: const SizedBox(), - ) - : const IgnorePointer( - key: Key('empty_lrc'), - child: SizedBox(), - ), - ), - ), - ], + } + }, + onScaleEnd: (details) { + final lrcState = Lyrics.inst.lrcViewKey?.currentState; + if (lrcState != null) { + final pps = details.velocity.pixelsPerSecond; + if (pps.dx > 0 || pps.dy > 0) { + lrcState.enterFullScreen(); + } + } + _lrcAdditionalScale.value = 0.0; + }, + child: _AnimatingThumnailWidget( + cp: cp, + isLocal: true, + displayLyrics: true, + fallback: _TrackImage( + track: track, + cp: cp, + ), + ), + ), + ), + ); + } +} + +class _AnimatingThumnailWidget extends StatelessWidget { + final double cp; + final bool isLocal; + final bool displayLyrics; + final Widget fallback; + const _AnimatingThumnailWidget({super.key, required this.cp, required this.isLocal, required this.fallback, required this.displayLyrics}); + + @override + Widget build(BuildContext context) { + final lyricsWidget = displayLyrics + ? Obx( + () => AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: settings.enableLyrics.valueR && (Lyrics.inst.currentLyricsLRC.valueR != null || Lyrics.inst.currentLyricsText.valueR != '') + ? LyricsLRCParsedView( + key: Lyrics.inst.lrcViewKey, + cp: cp, + initialLrc: Lyrics.inst.currentLyricsLRC.valueR, + videoOrImage: const SizedBox(), + ) + : const IgnorePointer( + key: Key('empty_lrc'), + child: SizedBox(), + ), + ), + ) + : null; + return ObxO( + rx: settings.animatingThumbnailInversed, + builder: (isInversed) => ObxO( + rx: settings.animatingThumbnailScaleMultiplier, + builder: (userScaleMultiplier) => ObxO( + rx: Player.inst.videoPlayerInfo, + builder: (videoInfo) { + final videoOrImage = videoInfo != null && videoInfo.isInitialized + ? BorderRadiusClip( + borderRadius: BorderRadius.circular((6.0 + 10.0 * cp).multipliedRadius), + child: DoubleTapDetector( + onDoubleTap: () => VideoController.inst.toggleFullScreenVideoView(isLocal: isLocal), + child: NamidaAspectRatio( + aspectRatio: videoInfo.aspectRatio, + child: Texture(textureId: videoInfo.textureId), ), + ), + ) + : fallback; + final animatedScaleChild = Stack( + alignment: Alignment.center, + children: [ + videoOrImage, + if (lyricsWidget != null) lyricsWidget, + ], + ); + return ObxO( + rx: VideoController.inst.videoZoomAdditionalScale, + builder: (videoZoomAdditionalScale) { + final additionalScaleVideo = 0.02 * videoZoomAdditionalScale; + return ObxO( + rx: _lrcAdditionalScale, + builder: (lrcAdditionalScale) { + final additionalScaleLRC = 0.02 * lrcAdditionalScale; + return ObxO( + rx: Player.inst.nowPlayingPosition, + builder: (nowPlayingPosition) { + final finalScale = additionalScaleLRC + additionalScaleVideo + WaveformController.inst.getCurrentAnimatingScale(nowPlayingPosition); + return AnimatedScale( + duration: const Duration(milliseconds: 100), + scale: (isInversed ? 1.22 - finalScale : 1.13 + finalScale) * userScaleMultiplier, + child: animatedScaleChild, + ); + }, ); }, ); }, - ), - ), - ); - }, + ); + }, + ), + ), ); } } @@ -756,42 +738,25 @@ class _AnimatingYoutubeIDImage extends StatelessWidget { @override Widget build(BuildContext context) { - return Obx(() { - final additionalScaleVideo = 0.02 * VideoController.inst.videoZoomAdditionalScale.value; - final finalScale = additionalScaleVideo + WaveformController.inst.getCurrentAnimatingScale(Player.inst.nowPlayingPosition); - final isInversed = settings.animatingThumbnailInversed.value; - final userScaleMultiplier = settings.animatingThumbnailScaleMultiplier.value; - final videoInfo = Player.inst.videoPlayerInfo; - return AnimatedScale( - duration: const Duration(milliseconds: 100), - scale: (isInversed ? 1.22 - finalScale : 1.13 + finalScale) * userScaleMultiplier, - child: videoInfo != null && videoInfo.isInitialized - ? Stack( - alignment: Alignment.center, - children: [ - BorderRadiusClip( - borderRadius: BorderRadius.circular((6.0 + 10.0 * cp).multipliedRadius), - child: DoubleTapDetector( - onDoubleTap: () => VideoController.inst.toggleFullScreenVideoView(isLocal: true), - child: NamidaAspectRatio( - aspectRatio: videoInfo.aspectRatio, - child: Texture(textureId: videoInfo.textureId), - ), - ), - ), - ], - ) - : _YoutubeIDImage( - video: video, - cp: cp, - ), - ); - }); + return _AnimatingThumnailWidget( + cp: cp, + isLocal: false, + displayLyrics: false, + fallback: _YoutubeIDImage( + video: video, + cp: cp, + ), + ); } } class Wallpaper extends StatefulWidget { - const Wallpaper({Key? key, this.child, this.particleOpacity = .1, this.gradient = true}) : super(key: key); + const Wallpaper({ + super.key, + this.child, + this.particleOpacity = .1, + this.gradient = true, + }); final Widget? child; final double particleOpacity; @@ -822,40 +787,39 @@ class _WallpaperState extends State with TickerProviderStateMixin { ), ), if (settings.enableMiniplayerParticles.value) - Obx( - () { - final playing = Player.inst.isPlaying; - return AnimatedOpacity( - duration: const Duration(seconds: 1), - opacity: playing ? 1 : 0, - child: Obx( - () { - final scale = WaveformController.inst.getCurrentAnimatingScale(Player.inst.nowPlayingPosition); - final bpm = (2000 * scale).withMinimum(0); - return AnimatedScale( - duration: const Duration(milliseconds: 300), - scale: 1.0 + scale * 1.5, - child: AnimatedBackground( - vsync: this, - behaviour: RandomParticleBehaviour( - options: ParticleOptions( - baseColor: context.theme.colorScheme.tertiary, - spawnMaxRadius: 4, - spawnMinRadius: 2, - spawnMaxSpeed: 60 + bpm * 2, - spawnMinSpeed: bpm, - maxOpacity: widget.particleOpacity, - minOpacity: 0, - particleCount: 50, - ), + ObxO( + rx: Player.inst.isPlaying, + builder: (playing) => AnimatedOpacity( + duration: const Duration(seconds: 1), + opacity: playing ? 1 : 0, + child: ObxO( + rx: Player.inst.nowPlayingPosition, + builder: (nowPlayingPosition) { + final scale = WaveformController.inst.getCurrentAnimatingScale(nowPlayingPosition); + final bpm = (2000 * scale).withMinimum(0); + return AnimatedScale( + duration: const Duration(milliseconds: 300), + scale: 1.0 + scale * 1.5, + child: AnimatedBackground( + vsync: this, + behaviour: RandomParticleBehaviour( + options: ParticleOptions( + baseColor: context.theme.colorScheme.tertiary, + spawnMaxRadius: 4, + spawnMinRadius: 2, + spawnMaxSpeed: 60 + bpm * 2, + spawnMinSpeed: bpm, + maxOpacity: widget.particleOpacity, + minOpacity: 0, + particleCount: 50, ), - child: const SizedBox(), ), - ); - }, - ), - ); - }, + child: const SizedBox(), + ), + ); + }, + ), + ), ), if (widget.child != null) widget.child!, ], diff --git a/lib/packages/miniplayer_base.dart b/lib/packages/miniplayer_base.dart index a3435682..14442d3d 100644 --- a/lib/packages/miniplayer_base.dart +++ b/lib/packages/miniplayer_base.dart @@ -5,7 +5,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/controller/lyrics_controller.dart'; +import 'package:namida/core/utils.dart'; +import 'package:namida/ui/dialogs/set_lrc_dialog.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; import 'package:namida/class/track.dart'; @@ -79,9 +81,8 @@ class MiniplayerTextData { } class NamidaMiniPlayerBase extends StatefulWidget { - final List queue; final double queueItemExtent; - final (Widget, Key) Function(BuildContext context, int index, int currentIndex) itemBuilder; + final (Widget, Key) Function(BuildContext context, int index, int currentIndex, List queue) itemBuilder; final int Function(E currentItem)? getDurationMS; final String Function(int number) itemsKeyword; final void Function(E currentItem) onAddItemsTap; @@ -89,7 +90,6 @@ class NamidaMiniPlayerBase extends StatefulWidget { final void Function(E currentItem) onTopTextTap; final void Function(E currentItem, TapUpDetails details) onMenuOpen; final FocusedMenuOptions focusedMenuOptions; - final Widget Function(E currentItem) extraActionButton; final Widget Function(E item, double cp) imageBuilder; final Widget Function(E item, double bcp) currentImageBuilder; final MiniplayerTextData Function(E item) textBuilder; @@ -97,7 +97,6 @@ class NamidaMiniPlayerBase extends StatefulWidget { const NamidaMiniPlayerBase({ super.key, - required this.queue, required this.queueItemExtent, required this.itemBuilder, required this.getDurationMS, @@ -107,7 +106,6 @@ class NamidaMiniPlayerBase extends StatefulWidget { required this.onTopTextTap, required this.onMenuOpen, required this.focusedMenuOptions, - required this.extraActionButton, required this.imageBuilder, required this.currentImageBuilder, required this.textBuilder, @@ -123,10 +121,7 @@ class _NamidaMiniPlayerBaseState extends State> { final isLoadingMore = false.obs; static const animationDuration = Duration(milliseconds: 150); - List get _currentQueue => widget.queue; - E get _getcurrentItem => widget.queue[Player.inst.currentIndex]; - - int _getDurationMS(E item) => Player.inst.currentItemDuration?.inMilliseconds ?? widget.getDurationMS?.call(item) ?? 0; + E get _getcurrentItem => Player.inst.currentQueue.value[Player.inst.currentIndex.value] as E; @override void dispose() { @@ -137,12 +132,12 @@ class _NamidaMiniPlayerBaseState extends State> { int refine(int index) { if (index <= -1) { - return _currentQueue.length - 1; - } - if (index >= _currentQueue.length) { + return Player.inst.currentQueue.value.length - 1; + } else if (index >= Player.inst.currentQueue.value.length) { return 0; + } else { + return index; } - return index; } @override @@ -150,15 +145,251 @@ class _NamidaMiniPlayerBaseState extends State> { final onSecondary = context.theme.colorScheme.onSecondaryContainer; const waveformChild = WaveformMiniplayer(); + final topRightButton = IconButton( + onPressed: () {}, + icon: TapDetector( + onTap: null, + initializer: (instance) { + void tapUp(TapUpDetails details) => widget.onMenuOpen(_getcurrentItem, details); + instance + ..onTapUp = tapUp + ..gestureSettings = MediaQuery.maybeGestureSettingsOf(context); + }, + child: Container( + padding: const EdgeInsets.all(4.0), + decoration: BoxDecoration( + color: context.theme.colorScheme.secondary.withOpacity(.2), + shape: BoxShape.circle, + ), + child: Icon(Broken.more, color: onSecondary), + ), + ), + iconSize: 22.0, + ); + + final topLeftButton = IconButton( + onPressed: MiniPlayerController.inst.snapToMini, + icon: Icon(Broken.arrow_down_2, color: onSecondary), + iconSize: 22.0, + ); + + const partyContainersChild = Stack( + children: [ + NamidaPartyContainer( + height: 2, + spreadRadiusMultiplier: 0.8, + ), + NamidaPartyContainer( + width: 2, + spreadRadiusMultiplier: 0.25, + ), + Align( + alignment: Alignment.bottomCenter, + child: NamidaPartyContainer( + height: 2, + spreadRadiusMultiplier: 0.8, + ), + ), + Align( + alignment: Alignment.centerRight, + child: NamidaPartyContainer( + width: 2, + spreadRadiusMultiplier: 0.25, + ), + ), + ], + ); + final positionTextChild = TapDetector( + onTap: () => Player.inst.seekSecondsBackward(), + child: LongPressDetector( + onLongPress: () => Player.inst.seek(Duration.zero), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Obx( + () { + final seek = MiniPlayerController.inst.seekValue.valueR; + final diffInMs = seek - Player.inst.nowPlayingPositionR; + final plusOrMinus = diffInMs < 0 ? '' : '+'; + final seekText = seek == 0 ? '00:00' : diffInMs.milliSecondsLabel; + return Text( + "$plusOrMinus$seekText", + style: context.textTheme.displaySmall?.copyWith(fontSize: 10.0), + ).animateEntrance( + showWhen: seek != 0, + durationMS: 700, + allCurves: Curves.easeInOutQuart, + ); + }, + ), + NamidaHero( + tag: 'MINIPLAYER_POSITION', + child: Obx( + () => Text( + Player.inst.nowPlayingPositionR.milliSecondsLabel, + style: context.textTheme.displaySmall, + ), + ), + ), + ], + ), + ), + ), + ); + final buttonsRowChild = Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.max, + children: [ + const RepeatModeIconButton(), + const EqualizerIconButton(), + LongPressDetector( + onLongPress: () { + showLRCSetDialog(_getcurrentItem as Playable, CurrentColor.inst.miniplayerColor); + }, + child: IconButton( + visualDensity: VisualDensity.compact, + style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), + padding: const EdgeInsets.all(2.0), + onPressed: () { + settings.save(enableLyrics: !settings.enableLyrics.value); + Lyrics.inst.updateLyrics(_getcurrentItem as Playable); + }, + icon: Obx( + () => settings.enableLyrics.valueR + ? Lyrics.inst.currentLyricsText.valueR == '' && Lyrics.inst.currentLyricsLRC.valueR == null + ? StackedIcon( + baseIcon: Broken.document, + secondaryText: !Lyrics.inst.lyricsCanBeAvailable.valueR ? 'x' : '?', + iconSize: 20.0, + blurRadius: 6.0, + baseIconColor: context.theme.colorScheme.onSecondaryContainer, + secondaryIconColor: context.theme.colorScheme.onSecondaryContainer, + ) + : Icon( + Broken.document, + size: 20.0, + color: context.theme.colorScheme.onSecondaryContainer, + ) + : Icon( + Broken.card_slash, + size: 20.0, + color: context.theme.colorScheme.onSecondaryContainer, + ), + ), + ), + ), + IconButton( + tooltip: lang.QUEUE, + visualDensity: VisualDensity.compact, + style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), + padding: const EdgeInsets.all(2.0), + onPressed: MiniPlayerController.inst.snapToQueue, + icon: Icon( + Broken.row_vertical, + size: 19.0, + color: context.theme.colorScheme.onSecondaryContainer, + ), + ), + const SizedBox(width: 6.0), + ], + ); + + final maxQueueHeight = MiniPlayerController.inst.maxOffset - 100.0 - MiniPlayerController.inst.topInset - 12.0; + + final queueChild = SafeArea( + bottom: false, + child: SizedBox( + height: context.height, + width: context.width, + child: Stack( + fit: StackFit.loose, + alignment: Alignment.bottomCenter, + children: [ + SizedBox( + height: maxQueueHeight, + child: BorderRadiusClip( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32.0.multipliedRadius), + topRight: Radius.circular(32.0.multipliedRadius), + ), + child: Obx( + () { + final queue = Player.inst.currentQueue.valueR; + final queueLength = queue.length; + if (queueLength == 0) return const SizedBox(); + final currentIndex = Player.inst.currentIndex.valueR; + final padding = + EdgeInsets.only(bottom: 8.0 + SelectedTracksController.inst.bottomPadding.valueR + kQueueBottomRowHeight + MediaQuery.paddingOf(context).bottom); + + return NamidaListView( + key: const Key('minikuru'), + scrollController: MiniPlayerController.inst.queueScrollController, + itemCount: queueLength, + itemExtent: widget.queueItemExtent, + onReorderStart: (index) => MiniPlayerController.inst.invokeStartReordering(), + onReorderEnd: (index) => MiniPlayerController.inst.invokeDoneReordering(), + onReorder: (oldIndex, newIndex) => Player.inst.reorderTrack(oldIndex, newIndex), + padding: padding, + itemBuilder: (context, i) { + final childWK = widget.itemBuilder(context, i, currentIndex, queue); + return FadeDismissible( + key: Key("Diss_${i}_${childWK.$2}_${queue.length}"), // queue length only for when removing current item and next is the same. + onDismissed: (direction) { + Player.inst.removeFromQueue(i); + MiniPlayerController.inst.invokeDoneReordering(); + }, + onDismissStart: (_) => MiniPlayerController.inst.invokeStartReordering(), + onDismissEnd: (_) => MiniPlayerController.inst.invokeDoneReordering(), + child: childWK.$1, + ); + }, + ); + }, + ), + ), + ), + Container( + width: context.width, + height: kQueueBottomRowHeight + MediaQuery.paddingOf(context).bottom, + decoration: BoxDecoration( + color: context.theme.scaffoldBackgroundColor, + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.0.multipliedRadius), + ), + ), + child: Padding( + padding: const EdgeInsets.all(4.0).add(EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom)), + child: FittedBox( + child: QueueUtilsRow( + itemsKeyword: widget.itemsKeyword, + onAddItemsTap: () => widget.onAddItemsTap(_getcurrentItem), + scrollQueueWidget: ObxO( + rx: MiniPlayerController.inst.arrowIcon, + builder: (arrow) => NamidaButton( + onPressed: MiniPlayerController.inst.animateQueueToCurrentTrack, + icon: arrow, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ); return Obx( () { - final currentIndex = Player.inst.currentIndex; + final currentIndex = Player.inst.currentIndex.valueR; + final queue = Player.inst.currentQueue.valueR; final indminus = refine(currentIndex - 1); final indplus = refine(currentIndex + 1); - final prevItem = _currentQueue.isEmpty ? null : _currentQueue[indminus]; - final currentItem = widget.queue[currentIndex]; - final nextItem = _currentQueue.isEmpty ? null : _currentQueue[indplus]; - final currentDurationInMS = _getDurationMS(currentItem); + final prevItem = queue.isEmpty ? null : queue[indminus] as E; + final currentItem = queue[currentIndex] as E; + final nextItem = queue.isEmpty ? null : queue[indplus] as E; + final currentDurationInMS = Player.inst.currentItemDuration.valueR?.inMilliseconds ?? widget.getDurationMS?.call(currentItem) ?? 0; final prevText = prevItem == null ? null : widget.textBuilder(prevItem); final currentText = widget.textBuilder(currentItem); @@ -167,34 +398,6 @@ class _NamidaMiniPlayerBaseState extends State> { final topText = widget.topText(currentItem); final videoIconBuilder = widget.focusedMenuOptions.videoIconBuilder(currentItem, 18.0, onSecondary); final focusedMenuBuilder = widget.focusedMenuOptions.builder(currentItem); - final extraActionButton = widget.extraActionButton(currentItem); - - const partyContainersChild = Stack( - children: [ - NamidaPartyContainer( - height: 2, - spreadRadiusMultiplier: 0.8, - ), - NamidaPartyContainer( - width: 2, - spreadRadiusMultiplier: 0.25, - ), - Align( - alignment: Alignment.bottomCenter, - child: NamidaPartyContainer( - height: 2, - spreadRadiusMultiplier: 0.8, - ), - ), - Align( - alignment: Alignment.centerRight, - child: NamidaPartyContainer( - width: 2, - spreadRadiusMultiplier: 0.25, - ), - ), - ], - ); final topRowChild = SafeArea( child: Padding( @@ -202,11 +405,7 @@ class _NamidaMiniPlayerBaseState extends State> { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - IconButton( - onPressed: MiniPlayerController.inst.snapToMini, - icon: Icon(Broken.arrow_down_2, color: onSecondary), - iconSize: 22.0, - ), + topLeftButton, Expanded( child: NamidaInkWell( borderRadius: 14.0, @@ -216,10 +415,10 @@ class _NamidaMiniPlayerBaseState extends State> { mainAxisSize: MainAxisSize.min, children: [ Text( - "${currentIndex + 1}/${_currentQueue.length}", + "${currentIndex + 1}/${queue.length}", style: TextStyle( color: onSecondary.withOpacity(.8), - fontSize: 12.0.multipliedFontScale, + fontSize: 12.0, fontWeight: FontWeight.w500, ), ), @@ -229,33 +428,13 @@ class _NamidaMiniPlayerBaseState extends State> { maxLines: 1, softWrap: false, overflow: TextOverflow.ellipsis, - style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16.0.multipliedFontScale, color: onSecondary.withOpacity(.9)), + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16.0, color: onSecondary.withOpacity(.9)), ), ], ), ), ), - IconButton( - onPressed: () {}, - icon: TapDetector( - onTap: null, - initializer: (instance) { - void tapUp(TapUpDetails details) => widget.onMenuOpen(_getcurrentItem, details); - instance - ..onTapUp = tapUp - ..gestureSettings = MediaQuery.maybeGestureSettingsOf(context); - }, - child: Container( - padding: const EdgeInsets.all(4.0), - decoration: BoxDecoration( - color: context.theme.colorScheme.secondary.withOpacity(.2), - shape: BoxShape.circle, - ), - child: Icon(Broken.more, color: onSecondary), - ), - ), - iconSize: 22.0, - ), + topRightButton, ], ), ), @@ -264,45 +443,7 @@ class _NamidaMiniPlayerBaseState extends State> { final positionDurationRowChild = Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - TapDetector( - onTap: () => Player.inst.seekSecondsBackward(), - child: LongPressDetector( - onLongPress: () => Player.inst.seek(Duration.zero), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Obx( - () { - final seek = MiniPlayerController.inst.seekValue.value; - final diffInMs = seek - Player.inst.nowPlayingPosition; - final plusOrMinus = diffInMs < 0 ? '' : '+'; - final seekText = seek == 0 ? '00:00' : diffInMs.milliSecondsLabel; - return Text( - "$plusOrMinus$seekText", - style: context.textTheme.displaySmall?.copyWith(fontSize: 10.0.multipliedFontScale), - ).animateEntrance( - showWhen: seek != 0, - durationMS: 700, - allCurves: Curves.easeInOutQuart, - ); - }, - ), - NamidaHero( - tag: 'MINIPLAYER_POSITION', - child: Obx( - () => Text( - Player.inst.nowPlayingPosition.milliSecondsLabel, - style: context.textTheme.displaySmall, - ), - ), - ), - ], - ), - ), - ), - ), + positionTextChild, TapDetector( onTap: () => Player.inst.seekSecondsForward(), child: Padding( @@ -311,10 +452,13 @@ class _NamidaMiniPlayerBaseState extends State> { tag: 'MINIPLAYER_DURATION', child: Obx( () { - final displayRemaining = settings.player.displayRemainingDurInsteadOfTotal.value; - final toSubtract = displayRemaining ? Player.inst.nowPlayingPosition : 0; + int toSubtract = 0; + String prefix = ''; + if (settings.player.displayRemainingDurInsteadOfTotal.valueR) { + toSubtract = Player.inst.nowPlayingPositionR; + prefix = '-'; + } final msToDisplay = currentDurationInMS - toSubtract; - final prefix = displayRemaining ? '-' : ''; return Text( "$prefix ${msToDisplay.milliSecondsLabel}", style: context.textTheme.displaySmall, @@ -327,29 +471,6 @@ class _NamidaMiniPlayerBaseState extends State> { ], ); - final buttonsRowChild = Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.max, - children: [ - const RepeatModeIconButton(), - const EqualizerIconButton(), - extraActionButton, - IconButton( - tooltip: lang.QUEUE, - visualDensity: VisualDensity.compact, - style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), - padding: const EdgeInsets.all(2.0), - onPressed: MiniPlayerController.inst.snapToQueue, - icon: Icon( - Broken.row_vertical, - size: 19.0, - color: context.theme.colorScheme.onSecondaryContainer, - ), - ), - const SizedBox(width: 6.0), - ], - ); - final bottomLeftButton = Expanded( child: Stack( alignment: Alignment.centerLeft, @@ -374,7 +495,7 @@ class _NamidaMiniPlayerBaseState extends State> { ), menuWidget: Obx( () { - final availableVideos = widget.focusedMenuOptions.localVideos; + final availableVideos = widget.focusedMenuOptions.localVideos.valueR; final ytVideos = widget.focusedMenuOptions.streamVideos.where((s) => s.formatSuffix != 'webm'); return ListView( padding: const EdgeInsets.symmetric(vertical: 12.0), @@ -384,7 +505,7 @@ class _NamidaMiniPlayerBaseState extends State> { title: lang.CHECK_FOR_MORE, icon: Broken.chart, bgColor: null, - trailing: isLoadingMore.value ? const LoadingIndicator() : null, + trailing: isLoadingMore.valueR ? const LoadingIndicator() : null, onTap: () async { isLoadingMore.value = true; await widget.focusedMenuOptions.loadQualities!(currentItem); @@ -396,7 +517,7 @@ class _NamidaMiniPlayerBaseState extends State> { final localOrCache = element.ytID == null ? lang.LOCAL : lang.CACHE; return Obx( () { - final isCurrent = element.path == (VideoController.inst.currentVideo.value?.path ?? Player.inst.currentCachedVideo?.path); + final isCurrent = element.path == (VideoController.inst.currentVideo.valueR?.path ?? Player.inst.currentCachedVideo.valueR?.path); return _MPQualityButton( onTap: () => widget.focusedMenuOptions.onLocalVideoTap(currentItem, element), bgColor: isCurrent ? CurrentColor.inst.miniplayerColor.withAlpha(20) : null, @@ -444,7 +565,7 @@ class _NamidaMiniPlayerBaseState extends State> { () { return AnimatedDecoration( duration: animationDuration, - decoration: isMenuOpened.value + decoration: isMenuOpened.valueR ? BoxDecoration( color: context.theme.scaffoldBackgroundColor, borderRadius: BorderRadius.circular(24.0.multipliedRadius), @@ -457,86 +578,88 @@ class _NamidaMiniPlayerBaseState extends State> { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Container( - padding: const EdgeInsets.all(6.0), + DecoratedBox( decoration: BoxDecoration( color: context.theme.colorScheme.secondaryContainer, shape: BoxShape.circle, ), - child: NamidaIconButton( - horizontalPadding: 0.0, - icon: null, - child: videoIconBuilder, - onPressed: () { - String toPercentage(double val) => "${(val * 100).toStringAsFixed(0)}%"; + child: Padding( + padding: const EdgeInsets.all(6.0), + child: NamidaIconButton( + horizontalPadding: 0.0, + icon: null, + child: videoIconBuilder, + onPressed: () { + String toPercentage(double val) => "${(val * 100).toStringAsFixed(0)}%"; - Widget getTextWidget(IconData icon, String title, double value) { - return Row( - children: [ - Icon(icon, color: context.defaultIconColor(CurrentColor.inst.miniplayerColor)), - const SizedBox(width: 12.0), - Text( - title, - style: context.textTheme.displayLarge, - ), - const SizedBox(width: 8.0), - Text( - toPercentage(value), - style: context.textTheme.displayMedium, - ) - ], - ); - } + Widget getTextWidget(IconData icon, String title, double value) { + return Row( + children: [ + Icon(icon, color: context.defaultIconColor(CurrentColor.inst.miniplayerColor)), + const SizedBox(width: 12.0), + NamidaButtonText( + title, + style: context.textTheme.displayLarge, + ), + const SizedBox(width: 8.0), + NamidaButtonText( + toPercentage(value), + style: context.textTheme.displayMedium, + ) + ], + ); + } - Widget getSlider({ - double min = 0.0, - double max = 2.0, - required double value, - required void Function(double newValue)? onChanged, - }) { - return Slider.adaptive( - min: min, - max: max, - value: value.clamp(min, max), - onChanged: onChanged, - divisions: (max * 100).round(), - label: "${(value * 100).toStringAsFixed(0)}%", - ); - } + Widget getSlider({ + double min = 0.0, + double max = 2.0, + required double value, + required void Function(double newValue)? onChanged, + }) { + return Slider.adaptive( + min: min, + max: max, + value: value.clamp(min, max), + onChanged: onChanged, + divisions: (max * 100).round(), + label: "${(value * 100).toStringAsFixed(0)}%", + ); + } - NamidaNavigator.inst.navigateDialog( - dialog: CustomBlurryDialog( - title: lang.CONFIGURE, - contentPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0), - actions: [ - NamidaIconButton( - icon: Broken.refresh, - onPressed: () { - const val = 1.0; - Player.inst.setPlayerPitch(val); - Player.inst.setPlayerSpeed(val); - Player.inst.setPlayerVolume(val); - settings.player.save( - pitch: val, - speed: val, - volume: val, - ); - }, + NamidaNavigator.inst.navigateDialog( + dialog: CustomBlurryDialog( + title: lang.CONFIGURE, + contentPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0), + actions: [ + NamidaIconButton( + icon: Broken.refresh, + onPressed: () { + const val = 1.0; + Player.inst.setPlayerPitch(val); + Player.inst.setPlayerSpeed(val); + Player.inst.setPlayerVolume(val); + settings.player.save( + pitch: val, + speed: val, + volume: val, + ); + }, + ), + NamidaButton( + text: lang.DONE, + onPressed: () { + NamidaNavigator.inst.closeDialog(); + }, + ) + ], + child: const EqualizerMainSlidersColumn( + verticalInBetweenPadding: 18.0, + tapToUpdate: false, ), - NamidaButton( - text: lang.DONE, - onPressed: () { - NamidaNavigator.inst.closeDialog(); - }, - ) - ], - child: const EqualizerMainSlidersColumn( - verticalInBetweenPadding: 18.0, - tapToUpdate: false, ), - ), - ); - }, + ); + }, + ), ), ), const SizedBox(width: 8.0), @@ -566,90 +689,6 @@ class _NamidaMiniPlayerBaseState extends State> { ), ); - final queueChild = SafeArea( - bottom: false, - child: SizedBox( - height: context.height, - width: context.width, - child: Stack( - fit: StackFit.loose, - alignment: Alignment.bottomCenter, - children: [ - SizedBox( - height: MiniPlayerController.inst.maxOffset - 100.0 - MiniPlayerController.inst.topInset - 12.0, - child: BorderRadiusClip( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32.0.multipliedRadius), - topRight: Radius.circular(32.0.multipliedRadius), - ), - child: Obx( - () { - final currentIndex = Player.inst.currentIndex; - final padding = - EdgeInsets.only(bottom: 8.0 + SelectedTracksController.inst.bottomPadding.value + kQueueBottomRowHeight + MediaQuery.paddingOf(context).bottom); - - return NamidaListView( - key: const Key('minikuru'), - scrollController: MiniPlayerController.inst.queueScrollController, - itemCount: widget.queue.length, - itemExtents: List.filled(widget.queue.length, widget.queueItemExtent), - onReorderStart: (index) => MiniPlayerController.inst.invokeStartReordering(), - onReorderEnd: (index) => MiniPlayerController.inst.invokeDoneReordering(), - onReorder: (oldIndex, newIndex) => Player.inst.reorderTrack(oldIndex, newIndex), - padding: padding, - itemBuilder: (context, i) { - final childWK = widget.itemBuilder(context, i, currentIndex); - return FadeDismissible( - key: Key("Diss_${i}_${childWK.$2}"), - onDismissed: (direction) { - Player.inst.removeFromQueue(i); - MiniPlayerController.inst.invokeDoneReordering(); - }, - onUpdate: (details) { - final isReordering = details.progress != 0.0; - if (isReordering) { - MiniPlayerController.inst.invokeStartReordering(); - } else { - MiniPlayerController.inst.invokeDoneReordering(); - } - }, - child: childWK.$1, - ); - }, - ); - }, - ), - ), - ), - Container( - width: context.width, - height: kQueueBottomRowHeight + MediaQuery.paddingOf(context).bottom, - decoration: BoxDecoration( - color: context.theme.scaffoldBackgroundColor, - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.0.multipliedRadius), - ), - ), - child: Padding( - padding: const EdgeInsets.all(4.0).add(EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom)), - child: FittedBox( - child: QueueUtilsRow( - itemsKeyword: widget.itemsKeyword, - onAddItemsTap: () => widget.onAddItemsTap(_getcurrentItem), - scrollQueueWidget: Obx( - () => NamidaButton( - onPressed: MiniPlayerController.inst.animateQueueToCurrentTrack, - icon: MiniPlayerController.inst.arrowIcon.value, - ), - ), - ), - ), - ), - ), - ], - ), - ), - ); return MiniplayerRaw( builder: (maxOffset, bounceUp, bounceDown, topInset, bottomInset, screenSize, sAnim, sMaxOffset, stParallax, siParallax, p, cp, ip, icp, rp, rcp, qp, qcp, bp, bcp, borderRadius, slowOpacity, opacity, fastOpacity, miniplayerbottomnavheight, bottomOffset, navBarHeight) { @@ -708,9 +747,9 @@ class _NamidaMiniPlayerBaseState extends State> { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - Color.alphaBlend(context.theme.colorScheme.onBackground.withAlpha(100), CurrentColor.inst.miniplayerColor) + Color.alphaBlend(context.theme.colorScheme.onSurface.withAlpha(100), CurrentColor.inst.miniplayerColor) .withOpacity(velpy(a: .38, b: .28, c: icp)), - Color.alphaBlend(context.theme.colorScheme.onBackground.withAlpha(40), CurrentColor.inst.miniplayerColor) + Color.alphaBlend(context.theme.colorScheme.onSurface.withAlpha(40), CurrentColor.inst.miniplayerColor) .withOpacity(velpy(a: .1, b: .22, c: icp)), ], ), @@ -719,19 +758,20 @@ class _NamidaMiniPlayerBaseState extends State> { ), /// Smol progress bar - Obx( - () { - final w = currentDurationInMS == 0 ? 0 : Player.inst.nowPlayingPosition / currentDurationInMS; + ObxO( + rx: Player.inst.nowPlayingPosition, + builder: (nowPlayingPosition) { + final w = currentDurationInMS == 0 ? 0 : nowPlayingPosition / currentDurationInMS; return Container( height: 2 * (1 - cp), - width: w > 0 ? ((Get.width * w) * 0.9) : 0, + width: w > 0 ? ((context.width * w) * 0.9) : 0, margin: const EdgeInsets.symmetric(horizontal: 16.0), child: AnimatedDecoration( duration: const Duration(milliseconds: kThemeAnimationDurationMS), decoration: BoxDecoration( color: CurrentColor.inst.miniplayerColor, borderRadius: BorderRadius.circular(50), - // color: Color.alphaBlend(context.theme.colorScheme.onBackground.withAlpha(40), CurrentColor.inst.miniplayerColor) + // color: Color.alphaBlend(context.theme.colorScheme.onSurface.withAlpha(40), CurrentColor.inst.miniplayerColor) // .withOpacity(velpy(a: .3, b: .22, c: icp)), ), ), @@ -827,7 +867,7 @@ class _NamidaMiniPlayerBaseState extends State> { child: Center( child: Obx( () { - final isButtonHighlighed = MiniPlayerController.inst.isPlayPauseButtonHighlighted.value; + final isButtonHighlighed = MiniPlayerController.inst.isPlayPauseButtonHighlighted.valueR; return TapDetector( onTap: null, initializer: (instance) { @@ -870,13 +910,14 @@ class _NamidaMiniPlayerBaseState extends State> { children: [ IconButton( highlightColor: Colors.transparent, - onPressed: () => Player.inst.togglePlayPause(), + onPressed: Player.inst.togglePlayPause, icon: Padding( padding: EdgeInsets.all(6.0 * cp * rcp), - child: Obx( - () => AnimatedSwitcher( + child: ObxO( + rx: Player.inst.isPlaying, + builder: (isPlaying) => AnimatedSwitcher( duration: const Duration(milliseconds: 200), - child: Player.inst.isPlaying + child: isPlaying ? Icon( Broken.pause, size: iconSize, @@ -896,7 +937,7 @@ class _NamidaMiniPlayerBaseState extends State> { if (widget.canShowBuffering) IgnorePointer( child: Obx( - () => Player.inst.shouldShowLoadingIndicator + () => Player.inst.shouldShowLoadingIndicatorR ? ThreeArchedCircle( color: Colors.white.withAlpha(120), size: iconSize * 1.4, @@ -1127,12 +1168,12 @@ class _NamidaMiniPlayerBaseState extends State> { ), Visibility( - maintainState: true, + maintainState: true, // cuz rebuilding from scratch almost kills raster visible: qp > 0 && !bounceUp, child: Opacity( opacity: qp.clamp(0.0, 1.0), child: Transform.translate( - offset: Offset(0, (1 - qp) * maxOffset * 0.8), + offset: Offset(0, (1 - qp) * maxQueueHeight), child: queueChild, ), ), @@ -1148,7 +1189,7 @@ class _NamidaMiniPlayerBaseState extends State> { class _RawImageContainer extends StatelessWidget { const _RawImageContainer({ - Key? key, + super.key, required this.child, required this.bottomOffset, required this.maxOffset, @@ -1156,7 +1197,7 @@ class _RawImageContainer extends StatelessWidget { required this.cp, required this.p, required this.width, - }) : super(key: key); + }); final Widget child; final double width; @@ -1197,7 +1238,7 @@ class _TrackInfo extends StatelessWidget { final double maxOffset; const _TrackInfo({ - Key? key, + super.key, required this.textData, required this.cp, required this.qp, @@ -1205,7 +1246,7 @@ class _TrackInfo extends StatelessWidget { required this.screenSize, required this.bottomOffset, required this.maxOffset, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -1247,7 +1288,7 @@ class _TrackInfo extends StatelessWidget { maxLines: textData.secondLine == '' ? 2 : 1, overflow: TextOverflow.ellipsis, style: context.textTheme.displayMedium?.copyWith( - fontSize: velpy(a: 14.5, b: 20.0, c: p).multipliedFontScale, + fontSize: velpy(a: 14.5, b: 20.0, c: p), height: 1, ), ), @@ -1258,7 +1299,7 @@ class _TrackInfo extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: context.textTheme.displayMedium?.copyWith( - fontSize: velpy(a: 12.5, b: 15.0, c: p).multipliedFontScale, + fontSize: velpy(a: 12.5, b: 15.0, c: p), ), ), ], @@ -1298,8 +1339,13 @@ class WaveformMiniplayer extends StatelessWidget { const WaveformMiniplayer({super.key, this.fixPadding = false}); int get _currentDurationInMS { - final totalDur = Player.inst.currentItemDuration ?? (Player.inst.currentQueue.isNotEmpty ? Player.inst.nowPlayingTrack.duration.seconds : Duration.zero); - return totalDur.inMilliseconds; + final totalDur = Player.inst.currentItemDuration.value; + if (totalDur != null) return totalDur.inMilliseconds; + final current = Player.inst.currentItem.value; + if (current is Selectable) { + return current.track.duration * 1000; + } + return 0; } void onSeekDragUpdate(double deltax, double maxWidth) { @@ -1380,14 +1426,14 @@ class _MPQualityButton extends StatelessWidget { Text( title, style: context.textTheme.displayMedium?.copyWith( - fontSize: 13.0.multipliedFontScale, + fontSize: 13.0, ), ), if (subtitle != '') Text( subtitle, style: context.textTheme.displaySmall?.copyWith( - fontSize: 13.0.multipliedFontScale, + fontSize: 13.0, ), ), ], diff --git a/lib/packages/miniplayer_raw.dart b/lib/packages/miniplayer_raw.dart index bd76b986..206daf84 100644 --- a/lib/packages/miniplayer_raw.dart +++ b/lib/packages/miniplayer_raw.dart @@ -102,26 +102,23 @@ class MiniplayerRaw extends StatelessWidget { return builder(maxOffset - navBarHeight, bounceUp, bounceDown, topInset, bottomInset, screenSize, sAnim, sMaxOffset, stParallax, siParallax, p, cp, ip, icp, rp, rcp, qp, qcp, bp, bcp, borderRadius, slowOpacity, opacity, fastOpacity, miniplayerbottomnavheight, bottomOffset, navBarHeight); }); - return WillPopScope( - onWillPop: MiniPlayerController.inst.onWillPop, - child: Listener( - behavior: HitTestBehavior.translucent, - onPointerDown: MiniPlayerController.inst.onPointerDown, - onPointerMove: MiniPlayerController.inst.onPointerMove, - onPointerUp: MiniPlayerController.inst.onPointerUp, - child: enableHorizontalGestures - ? GestureDetector( - behavior: HitTestBehavior.deferToChild, - onTap: MiniPlayerController.inst.gestureDetectorOnTap, - onVerticalDragUpdate: MiniPlayerController.inst.gestureDetectorOnVerticalDragUpdate, - onVerticalDragEnd: (_) => MiniPlayerController.inst.verticalSnapping(), - onHorizontalDragStart: MiniPlayerController.inst.gestureDetectorOnHorizontalDragStart, - onHorizontalDragUpdate: MiniPlayerController.inst.gestureDetectorOnHorizontalDragUpdate, - onHorizontalDragEnd: MiniPlayerController.inst.gestureDetectorOnHorizontalDragEnd, - child: child, - ) - : child, - ), + return Listener( + behavior: HitTestBehavior.translucent, + onPointerDown: MiniPlayerController.inst.onPointerDown, + onPointerMove: MiniPlayerController.inst.onPointerMove, + onPointerUp: MiniPlayerController.inst.onPointerUp, + child: enableHorizontalGestures + ? GestureDetector( + behavior: HitTestBehavior.deferToChild, + onTap: MiniPlayerController.inst.gestureDetectorOnTap, + onVerticalDragUpdate: MiniPlayerController.inst.gestureDetectorOnVerticalDragUpdate, + onVerticalDragEnd: (_) => MiniPlayerController.inst.verticalSnapping(), + onHorizontalDragStart: MiniPlayerController.inst.gestureDetectorOnHorizontalDragStart, + onHorizontalDragUpdate: MiniPlayerController.inst.gestureDetectorOnHorizontalDragUpdate, + onHorizontalDragEnd: MiniPlayerController.inst.gestureDetectorOnHorizontalDragEnd, + child: child, + ) + : child, ); } } diff --git a/lib/packages/mp.dart b/lib/packages/mp.dart index 1d4a9406..a2407019 100644 --- a/lib/packages/mp.dart +++ b/lib/packages/mp.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/controller/wakelock_controller.dart'; import 'package:namida/core/extensions.dart'; @@ -92,7 +92,6 @@ class NamidaYTMiniplayerState extends State with SingleTicke bool _isDraggingDownwards = false; double get _percentageMultiplier => _alternativePercentage && _isDraggingDownwards ? 0.25 : 1.0; - bool _isDragManagedInternally = true; void updatePercentageMultiplier(bool alt) { _alternativePercentage = alt; } @@ -101,6 +100,11 @@ class NamidaYTMiniplayerState extends State with SingleTicke _startedDragAtHeight = _dragheight; } + bool _isDragManagedInternally = true; + void setDragExternally(bool external) { + _isDragManagedInternally = !external; + } + double? _startedDragAtHeight; @override @@ -150,7 +154,6 @@ class NamidaYTMiniplayerState extends State with SingleTicke } void _resetValues() { - _isDragManagedInternally = false; _alternativePercentage = false; _startedDragAtHeight = null; } @@ -221,14 +224,9 @@ class NamidaYTMiniplayerState extends State with SingleTicke alignment: Alignment.bottomCenter, child: GestureDetector( onTap: _dragheight == widget.minHeight ? () => animateToState(true) : null, - onVerticalDragStart: (details) { - _isDragManagedInternally = !_alternativePercentage; - }, onVerticalDragUpdate: (details) => onVerticalDragUpdate(details.delta.dy), - // onVerticalDragCancel: () => !_isDragManagedInternally ? null : animateToState(_wasExpanded), onVerticalDragEnd: (details) { if (_isDragManagedInternally) onVerticalDragEnd(details.velocity.pixelsPerSecond.dy); - _isDragManagedInternally = !_alternativePercentage; }, child: Material( clipBehavior: Clip.hardEdge, diff --git a/lib/packages/searchbar_animation.dart b/lib/packages/searchbar_animation.dart index 78017230..cd9bec46 100644 --- a/lib/packages/searchbar_animation.dart +++ b/lib/packages/searchbar_animation.dart @@ -114,6 +114,7 @@ class SearchBarAnimation extends StatefulWidget { final Radius? cursorRadius; const SearchBarAnimation({ + super.key, required this.textEditingController, required this.isOriginalAnimation, required this.trailingWidget, @@ -151,10 +152,9 @@ class SearchBarAnimation extends StatefulWidget { this.inputFormatters, this.onTap, this.onTapOutside, - Key? key, this.cursorHeight, this.cursorRadius, - }) : super(key: key); + }); @override SearchBarAnimationState createState() => SearchBarAnimationState(); diff --git a/lib/packages/three_arched_circle.dart b/lib/packages/three_arched_circle.dart index 34e9ffee..d4a756ce 100644 --- a/lib/packages/three_arched_circle.dart +++ b/lib/packages/three_arched_circle.dart @@ -1,4 +1,5 @@ /// source: https://github.com/watery-desert/loading_animation_widget +library; import 'package:flutter/material.dart'; import 'dart:math' as math; @@ -10,10 +11,10 @@ class ThreeArchedCircle extends StatefulWidget { final double size; final Color color; const ThreeArchedCircle({ - Key? key, + super.key, required this.color, required this.size, - }) : super(key: key); + }); @override State createState() => _ThreeArchedCircleState(); diff --git a/lib/ui/dialogs/add_to_playlist_dialog.dart b/lib/ui/dialogs/add_to_playlist_dialog.dart index 3458206c..9602b6e9 100644 --- a/lib/ui/dialogs/add_to_playlist_dialog.dart +++ b/lib/ui/dialogs/add_to_playlist_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/track.dart'; import 'package:namida/controller/navigator_controller.dart'; @@ -30,7 +30,7 @@ void showAddToPlaylistDialog(List tracks) { ), Text( lang.ADD_TO_PLAYLIST, - style: Get.theme.textTheme.displayMedium, + style: namida.theme.textTheme.displayMedium, maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -40,7 +40,7 @@ void showAddToPlaylistDialog(List tracks) { leftAction: Obx( () => Text( "${PlaylistController.inst.playlistsMap.length.formatDecimal()} ${lang.PLAYLISTS}", - style: Get.theme.textTheme.displayMedium, + style: namida.theme.textTheme.displayMedium, maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -52,8 +52,8 @@ void showAddToPlaylistDialog(List tracks) { ), ], child: SizedBox( - height: Get.height * 0.7, - width: Get.width, + height: namida.height * 0.7, + width: namida.width, child: PlaylistsPage( enableHero: true, tracksToAdd: tracks, diff --git a/lib/ui/dialogs/common_dialogs.dart b/lib/ui/dialogs/common_dialogs.dart index c8b37307..602f2d27 100644 --- a/lib/ui/dialogs/common_dialogs.dart +++ b/lib/ui/dialogs/common_dialogs.dart @@ -8,6 +8,7 @@ import 'package:namida/controller/navigator_controller.dart'; import 'package:namida/controller/playlist_controller.dart'; import 'package:namida/controller/settings_controller.dart'; import 'package:namida/core/constants.dart'; +import 'package:namida/core/dimensions.dart'; import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; @@ -61,7 +62,7 @@ class NamidaDialogs { "${tracks.displayTrackKeyword} & ${artists.length.displayArtistKeyword}", QueueSource.album, thirdLineText: artists.join(', ').overflow, - forceSquared: shouldAlbumBeSquared, + forceSquared: Dimensions.inst.shouldAlbumBeSquared, forceSingleArtwork: true, heroTag: 'album_$albumIdentifier', albumToAddFrom: (tracks.album, albumIdentifier), @@ -197,7 +198,7 @@ class NamidaDialogs { required bool recursiveTracks, }) async { if (recursiveTracks) Vibration.vibrate(duration: 20, amplitude: 50); - final tracks = recursiveTracks ? folder.tracksRecusive.toList() : folder.tracks; + final tracks = recursiveTracks ? folder.tracksRecusive().toList() : folder.tracks(); await showGeneralPopupDialog( tracks, folder.folderName, diff --git a/lib/ui/dialogs/edit_tags_dialog.dart b/lib/ui/dialogs/edit_tags_dialog.dart index c61cde5f..2a704c8b 100644 --- a/lib/ui/dialogs/edit_tags_dialog.dart +++ b/lib/ui/dialogs/edit_tags_dialog.dart @@ -1,6 +1,5 @@ import 'package:checkmark/checkmark.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/class/faudiomodel.dart'; import 'package:namida/class/track.dart'; @@ -18,6 +17,7 @@ import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/themes.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/main.dart'; import 'package:namida/ui/widgets/artwork.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; @@ -87,8 +87,8 @@ Future showSetYTLinkCommentDialog(List tracks, Color colorScheme) a contentPadding: EdgeInsets.zero, insetPadding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0), child: SizedBox( - width: Get.width, - height: Get.height * 0.7, + width: namida.width, + height: namida.height * 0.7, child: Column( children: [ const SizedBox(height: 8.0), @@ -139,7 +139,7 @@ Future showSetYTLinkCommentDialog(List tracks, Color colorScheme) a const CancelButton(), Obx( () => NamidaButton( - enabled: canEditComment.value && _editingInProgress[singleTrack.path] != true, + enabled: canEditComment.valueR && _editingInProgress[singleTrack.path] != true, textWidget: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -193,17 +193,18 @@ Future showSetYTLinkCommentDialog(List tracks, Color colorScheme) a ); } -Widget get _getKeepDatesWidget => NamidaIconButton( - tooltip: lang.KEEP_FILE_DATES, - icon: settings.editTagsKeepFileDates.value ? Broken.document_code_2 : Broken.calendar_edit, - onPressed: () { - settings.save(editTagsKeepFileDates: !settings.editTagsKeepFileDates.value); - }, - child: Obx( - () => StackedIcon( +Widget get _getKeepDatesWidget => ObxO( + rx: settings.editTagsKeepFileDates, + builder: (editTagsKeepFileDates) => NamidaIconButton( + tooltip: lang.KEEP_FILE_DATES, + icon: editTagsKeepFileDates ? Broken.document_code_2 : Broken.calendar_edit, + onPressed: () { + settings.save(editTagsKeepFileDates: !settings.editTagsKeepFileDates.value); + }, + child: StackedIcon( disableColor: true, baseIcon: Broken.document_code_2, - secondaryIcon: settings.editTagsKeepFileDates.value ? Broken.tick_circle : Broken.close_circle, + secondaryIcon: editTagsKeepFileDates ? Broken.tick_circle : Broken.close_circle, ), ), ); @@ -211,7 +212,7 @@ Widget get _getKeepDatesWidget => NamidaIconButton( Future _editSingleTrackTagsDialog(Track track, Color? colorScheme) async { if (!await requestManageStoragePermission()) return; - final color = (colorScheme ?? CurrentColor.inst.color).obs; + final color = (colorScheme ?? CurrentColor.inst.color).obso; if (colorScheme == null) { CurrentColor.inst.getTrackDelightnedColor(track, useIsolate: true).executeWithMinDelay().then((c) { if (c == color.value) return; @@ -292,11 +293,10 @@ Future _editSingleTrackTagsDialog(Track track, Color? colorScheme) async { }, scale: 0.94, lighterDialogColor: false, - dialog: StreamBuilder( - initialData: color.value, - stream: color.stream, - builder: (context, snapshot) { - final theme = AppThemes.inst.getAppTheme(snapshot.data, null, false); + dialog: ObxOContext( + rx: color, + builder: (context, color) { + final theme = AppThemes.inst.getAppTheme(color, null, false); return AnimatedTheme( data: theme, child: CustomBlurryDialog( @@ -314,27 +314,27 @@ Future _editSingleTrackTagsDialog(Track track, Color? colorScheme) async { subList.removeWhere((element) => settings.tagFieldsToEdit.contains(element)); await NamidaNavigator.inst.navigateDialog( + scale: 1.0, onDisposing: () { subList.close(); }, - scale: 0.94, dialog: CustomBlurryDialog( title: lang.TAG_FIELDS, child: SizedBox( - width: Get.width, - height: Get.height * 0.7, + width: namida.width, + height: namida.height * 0.6, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 6.0), - Text('${lang.ACTIVE} (${lang.REORDERABLE})', style: Get.textTheme.displayMedium), + Text('${lang.ACTIVE} (${lang.REORDERABLE})', style: namida.textTheme.displayMedium), const SizedBox(height: 6.0), Expanded( child: Obx( () { final tagFields = settings.tagFieldsToEdit; - return ReorderableListView.builder( - proxyDecorator: (child, index, animation) => child, + return NamidaListView( + itemExtent: null, padding: const EdgeInsets.only(bottom: 24.0), itemCount: settings.tagFieldsToEdit.length, onReorder: (oldIndex, newIndex) { @@ -348,7 +348,7 @@ Future _editSingleTrackTagsDialog(Track track, Color? colorScheme) async { itemBuilder: (context, i) { final tf = tagFields[i]; return Padding( - key: Key(i.toString()), + key: ValueKey(i), padding: const EdgeInsets.only(top: 8.0), child: ListTileWithCheckMark( active: true, @@ -370,7 +370,7 @@ Future _editSingleTrackTagsDialog(Track track, Color? colorScheme) async { ), ), const SizedBox(height: 12.0), - Text(lang.NON_ACTIVE, style: Get.textTheme.displayMedium), + Text(lang.NON_ACTIVE, style: namida.textTheme.displayMedium), const SizedBox(height: 6.0), Expanded( child: Obx( @@ -406,7 +406,7 @@ Future _editSingleTrackTagsDialog(Track track, Color? colorScheme) async { ], leftAction: NamidaInkWell( bgColor: theme.cardColor, - onTap: () => trimWhiteSpaces.value = !trimWhiteSpaces.value, + onTap: trimWhiteSpaces.toggle, padding: const EdgeInsets.all(8.0), child: Row( mainAxisSize: MainAxisSize.min, @@ -420,7 +420,7 @@ Future _editSingleTrackTagsDialog(Track track, Color? colorScheme) async { activeColor: theme.listTileTheme.iconColor!, inactiveColor: theme.listTileTheme.iconColor!, duration: const Duration(milliseconds: 400), - active: trimWhiteSpaces.value, + active: trimWhiteSpaces.valueR, ), ), ), @@ -429,7 +429,7 @@ Future _editSingleTrackTagsDialog(Track track, Color? colorScheme) async { ), Text( lang.REMOVE_WHITESPACES, - style: Get.textTheme.displaySmall, + style: namida.textTheme.displaySmall, ), ], ), @@ -437,7 +437,7 @@ Future _editSingleTrackTagsDialog(Track track, Color? colorScheme) async { actions: [ Obx( () => NamidaButton( - enabled: canEditTags.value && _editingInProgress[track.path] != true, + enabled: canEditTags.valueR && _editingInProgress[track.path] != true, icon: Broken.pen_add, textWidget: Row( mainAxisSize: MainAxisSize.min, @@ -480,8 +480,8 @@ Future _editSingleTrackTagsDialog(Track track, Color? colorScheme) async { crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - height: Get.height * 0.61, - width: Get.width, + height: namida.height * 0.61, + width: namida.width, child: ListView( padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom * 0.6), children: [ @@ -493,10 +493,10 @@ Future _editSingleTrackTagsDialog(Track track, Color? colorScheme) async { children: [ Obx( () => ArtworkWidget( - key: Key(currentImagePath.value), - thumbnailSize: Get.width / 3, - bytes: currentImagePath.value != '' ? null : artwork?.bytes, - path: currentImagePath.value != '' ? currentImagePath.value : null, + key: Key(currentImagePath.valueR), + thumbnailSize: namida.width / 3, + bytes: currentImagePath.valueR != '' ? null : artwork?.bytes, + path: currentImagePath.valueR != '' ? currentImagePath.valueR : null, onTopWidgets: [ Positioned( bottom: 0, @@ -526,7 +526,7 @@ Future _editSingleTrackTagsDialog(Track track, Color? colorScheme) async { child: Column( mainAxisSize: MainAxisSize.min, children: [ - ...settings.tagFieldsToEdit.take(2).map( + ...settings.tagFieldsToEdit.valueR.take(2).map( (e) => Padding( padding: const EdgeInsets.only(top: 10.0), child: getTagTextField(e), @@ -538,7 +538,7 @@ Future _editSingleTrackTagsDialog(Track track, Color? colorScheme) async { ], ), const SizedBox(height: 8.0), - ...settings.tagFieldsToEdit.sublist(2).map( + ...settings.tagFieldsToEdit.valueR.sublist(2).map( (e) => Padding( padding: const EdgeInsets.only(top: 12.0), child: getTagTextField(e), @@ -555,14 +555,14 @@ Future _editSingleTrackTagsDialog(Track track, Color? colorScheme) async { ), Text( track.path, - style: Get.textTheme.displaySmall, + style: namida.textTheme.displaySmall, ), const SizedBox( height: 4.0, ), Text( track.audioInfoFormatted, - style: Get.textTheme.displaySmall, + style: namida.textTheme.displaySmall, ), const SizedBox(height: 4.0), NamidaInkWell( @@ -589,8 +589,8 @@ Future _editSingleTrackTagsDialog(Track track, Color? colorScheme) async { const Icon(Broken.magicpen, size: 14.0), const SizedBox(width: 4.0), Text( - "${lang.AUTO_EXTRACT_TAGS_FROM_FILENAME} ${didAutoExtractFromFilename.value ? '✓' : ''}", - style: Get.textTheme.displaySmall?.copyWith( + "${lang.AUTO_EXTRACT_TAGS_FROM_FILENAME} ${didAutoExtractFromFilename.valueR ? '✓' : ''}", + style: namida.textTheme.displaySmall?.copyWith( decoration: TextDecoration.underline, decorationStyle: TextDecorationStyle.dashed, ), @@ -621,30 +621,31 @@ Future _editMultipleTracksTags(List tracksPre) async { padding: const EdgeInsets.symmetric(horizontal: 12.0), child: Text( lang.MULTIPLE_TRACKS_TAGS_EDIT_NOTE, - style: Get.textTheme.displayMedium, + style: namida.textTheme.displayMedium, ), ), const SizedBox(height: 12.0), SizedBox( - width: Get.width, - height: Get.height * 0.5, + width: namida.width, + height: namida.height * 0.5, child: ListView.builder( itemCount: tracks.length, itemBuilder: (context, index) { - final tr = tracks[index]; - return Obx( - () => TrackTile( + final tr = tracks.value[index]; + return ObxO( + rx: tracks, + builder: (tracksRaw) => TrackTile( index: index, trackOrTwd: tr, queueSource: QueueSource.allTracks, - onTap: () => tracks.addIf(() => !tracks.contains(tr), tr), - bgColor: tracks.contains(tr) ? null : Colors.black.withAlpha(0), + onTap: () { + if (!tracksRaw.contains(tr)) tracks.add(tr); + }, + bgColor: tracksRaw.contains(tr) ? null : Colors.black.withAlpha(0), trailingWidget: IconButton( icon: const Icon(Broken.close_circle), visualDensity: VisualDensity.compact, - onPressed: () { - tracks.remove(tr); - }, + onPressed: () => tracks.remove(tr), ), ), ); @@ -654,9 +655,9 @@ Future _editMultipleTracksTags(List tracksPre) async { ], ); - final RxBool trimWhiteSpaces = true.obs; - final RxBool canEditTags = false.obs; - final RxString currentImagePath = ''.obs; + final trimWhiteSpaces = true.obs; + final canEditTags = false.obs; + final currentImagePath = ''.obs; final tagsControllers = {}; final editedTags = {}; @@ -676,7 +677,7 @@ Future _editMultipleTracksTags(List tracksPre) async { ]; /// creating controllers - availableTagsToEdit.loop((at, index) { + availableTagsToEdit.loop((at) { tagsControllers[at] = TextEditingController(); }); void checkEmptyValues() { @@ -723,9 +724,9 @@ Future _editMultipleTracksTags(List tracksPre) async { actions: [ Obx( () { - final isEditing = tracks.any((track) => _editingInProgress[track.path] == true); + final isEditing = tracks.valueR.any((track) => _editingInProgress[track.path] == true); return NamidaButton( - enabled: canEditTags.value && !isEditing, + enabled: canEditTags.valueR && !isEditing, icon: Broken.pen_add, textWidget: Row( mainAxisSize: MainAxisSize.min, @@ -742,7 +743,7 @@ Future _editMultipleTracksTags(List tracksPre) async { ], ), onPressed: () { - tracks.loop((track, _) => _editingInProgress[track.path] = true); + tracks.loop((track) => _editingInProgress[track.path] = true); NamidaNavigator.inst.navigateDialog( dialog: CustomBlurryDialog( title: lang.NOTE, @@ -763,10 +764,10 @@ Future _editMultipleTracksTags(List tracksPre) async { editedTags.updateAll((key, value) => value.trimAll()); } - final RxInt successfullEdits = 0.obs; + final successfullEdits = 0.obs; final RxList failedEditsTracks = [].obs; - final RxBool finishedEditing = false.obs; - final RxString updatingLibrary = '?'.obs; + final finishedEditing = false.obs; + final updatingLibrary = '?'.obs; void showFailedTracksDialogs() { NamidaNavigator.inst.navigateDialog( @@ -780,8 +781,8 @@ Future _editMultipleTracksTags(List tracksPre) async { ) ], child: SizedBox( - height: Get.height * 0.5, - width: Get.width, + height: namida.height * 0.5, + width: namida.width, child: NamidaTracksList( padding: EdgeInsets.zero, queueLength: failedEditsTracks.length, @@ -804,7 +805,7 @@ Future _editMultipleTracksTags(List tracksPre) async { Widget getText(String text, {TextStyle? style}) { return Text( text, - style: style ?? Get.textTheme.displayMedium, + style: style ?? namida.textTheme.displayMedium, ); } @@ -829,7 +830,7 @@ Future _editMultipleTracksTags(List tracksPre) async { actions: [ Obx( () => NamidaButton( - enabled: finishedEditing.value, + enabled: finishedEditing.valueR, text: lang.DONE, onPressed: () => NamidaNavigator.inst.closeDialog(), ), @@ -840,7 +841,7 @@ Future _editMultipleTracksTags(List tracksPre) async { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - getText('${lang.SUCCEEDED}: ${successfullEdits.value}'), + getText('${lang.SUCCEEDED}: ${successfullEdits.valueR}'), const SizedBox(height: 8.0), Obx( () => Row( @@ -852,8 +853,8 @@ Future _editMultipleTracksTags(List tracksPre) async { onTap: showFailedTracksDialogs, child: getText( lang.CHECK_LIST, - style: Get.textTheme.displaySmall?.copyWith( - color: Get.theme.colorScheme.secondary, + style: namida.textTheme.displaySmall?.copyWith( + color: namida.theme.colorScheme.secondary, decoration: TextDecoration.underline, decorationStyle: TextDecorationStyle.solid, ), @@ -863,7 +864,7 @@ Future _editMultipleTracksTags(List tracksPre) async { ), ), const SizedBox(height: 8.0), - getText('${lang.UPDATING} ${updatingLibrary.value}'), + getText('${lang.UPDATING} ${updatingLibrary.valueR}'), const SizedBox(height: 8.0), ], ), @@ -873,7 +874,7 @@ Future _editMultipleTracksTags(List tracksPre) async { ); String? errorMsg; await FAudioTaggerController.inst.updateTracksMetadata( - tracks: tracks, + tracks: tracks.value, editedTags: editedTags, trimWhiteSpaces: trimWhiteSpaces.value, imagePath: currentImagePath.value, @@ -900,7 +901,7 @@ Future _editMultipleTracksTags(List tracksPre) async { updatingLibrary.value = '✓'; finishedEditing.value = true; canEditTags.value = false; - tracks.loop((track, _) => _editingInProgress[track.path] = false); + tracks.loop((track) => _editingInProgress[track.path] = false); }, ), ], @@ -913,8 +914,8 @@ Future _editMultipleTracksTags(List tracksPre) async { ) ], leftAction: NamidaInkWell( - bgColor: Get.theme.cardColor, - onTap: () => trimWhiteSpaces.value = !trimWhiteSpaces.value, + bgColor: namida.theme.cardColor, + onTap: trimWhiteSpaces.toggle, padding: const EdgeInsets.all(8.0), child: Row( mainAxisSize: MainAxisSize.min, @@ -925,10 +926,10 @@ Future _editMultipleTracksTags(List tracksPre) async { width: 18, child: CheckMark( strokeWidth: 2, - activeColor: Get.theme.listTileTheme.iconColor!, - inactiveColor: Get.theme.listTileTheme.iconColor!, + activeColor: namida.theme.listTileTheme.iconColor!, + inactiveColor: namida.theme.listTileTheme.iconColor!, duration: const Duration(milliseconds: 400), - active: trimWhiteSpaces.value, + active: trimWhiteSpaces.valueR, ), ), ), @@ -937,7 +938,7 @@ Future _editMultipleTracksTags(List tracksPre) async { ), Text( lang.REMOVE_WHITESPACES, - style: Get.textTheme.displaySmall, + style: namida.textTheme.displaySmall, ), ], ), @@ -945,7 +946,7 @@ Future _editMultipleTracksTags(List tracksPre) async { child: Obx( () => tracks.isEmpty ? SizedBox( - width: Get.width * 0.6, + width: namida.width * 0.6, child: NamidaButton( onPressed: () { NamidaNavigator.inst.navigateDialog( @@ -958,7 +959,7 @@ Future _editMultipleTracksTags(List tracksPre) async { ); }, textWidget: Obx( - () => Text(tracks.displayTrackKeyword), + () => Text(tracks.valueR.displayTrackKeyword), ), ), ) @@ -966,10 +967,10 @@ Future _editMultipleTracksTags(List tracksPre) async { crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - height: Get.height * 0.7, - width: Get.width, + height: namida.height * 0.7, + width: namida.width, child: ListView( - padding: Get.context == null ? null : EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(Get.context!).bottom * 0.6), + padding: EdgeInsets.only(bottom: (namida.viewInsets?.bottom ?? 0) * 0.6), children: [ Row( mainAxisSize: MainAxisSize.min, @@ -978,29 +979,29 @@ Future _editMultipleTracksTags(List tracksPre) async { alignment: Alignment.bottomRight, children: [ Obx( - () => currentImagePath.value != '' + () => currentImagePath.valueR != '' ? ArtworkWidget( - key: Key(currentImagePath.value), - thumbnailSize: Get.width / 3, - path: currentImagePath.value, + key: Key(currentImagePath.valueR), + thumbnailSize: namida.width / 3, + path: currentImagePath.valueR, ) : MultiArtworkContainer( heroTag: 'edittags_artwork', - size: Get.width / 3, - tracks: tracks.toImageTracks(), + size: namida.width / 3, + tracks: tracks.valueR.toImageTracks(), fallbackToFolderCover: false, onTopWidget: tracks.length > 3 ? Positioned( right: 0, bottom: 0, child: NamidaBlurryContainer( - width: Get.width / 6.2, - height: Get.width / 6.2, + width: namida.width / 6.2, + height: namida.width / 6.2, borderRadius: BorderRadius.zero, child: Center( child: Text( "+${tracks.length - 3}", - style: Get.textTheme.displayLarge, + style: namida.textTheme.displayLarge, ), ), ), @@ -1021,7 +1022,7 @@ Future _editMultipleTracksTags(List tracksPre) async { height: 8.0, ), SizedBox( - width: Get.width, + width: namida.width, child: NamidaButton( onPressed: () { NamidaNavigator.inst.navigateDialog( @@ -1040,7 +1041,7 @@ Future _editMultipleTracksTags(List tracksPre) async { ); }, textWidget: Obx( - () => Text(tracks.displayTrackKeyword), + () => Text(tracks.valueR.displayTrackKeyword), ), ), ), @@ -1048,7 +1049,7 @@ Future _editMultipleTracksTags(List tracksPre) async { height: 8.0, ), SizedBox( - width: Get.width, + width: namida.width, child: NamidaButton( text: lang.EDIT_ARTWORK, onPressed: () async { @@ -1088,27 +1089,30 @@ Future _editMultipleTracksTags(List tracksPre) async { height: 12.0, ), Obx( - () => Text( - [ - tracks.displayTrackKeyword, - tracks.totalSizeFormatted, - tracks.totalDurationFormatted, - ].join(' • '), - style: Get.textTheme.displaySmall, - ), + () { + final trs = tracks.valueR; + return Text( + [ + trs.displayTrackKeyword, + trs.totalSizeFormatted, + trs.totalDurationFormatted, + ].join(' • '), + style: namida.textTheme.displaySmall, + ); + }, ), const SizedBox( height: 8.0, ), Obx( - () => hasEmptyDumbValues.value - ? RichText( - text: TextSpan( + () => hasEmptyDumbValues.valueR + ? Text.rich( + TextSpan( children: [ - TextSpan(text: "${lang.WARNING}: ", style: Get.textTheme.displayMedium), + TextSpan(text: "${lang.WARNING}: ", style: namida.textTheme.displayMedium), TextSpan( text: lang.EMPTY_NON_MEANINGFUL_TAG_FIELDS, - style: Get.textTheme.displaySmall, + style: namida.textTheme.displaySmall, ), ], ), @@ -1185,7 +1189,7 @@ class _CustomTagTextFieldState extends State { maxLines: widget.maxLines, autovalidateMode: widget.validatorMode, keyboardType: widget.keyboardType ?? (widget.isNumeric ? TextInputType.number : null), - style: context.textTheme.displaySmall?.copyWith(fontSize: 14.5.multipliedFontScale, fontWeight: FontWeight.w600), + style: context.textTheme.displaySmall?.copyWith(fontSize: 14.5, fontWeight: FontWeight.w600), // onTapOutside: (event) => FocusScope.of(context).unfocus(), // inconvenient onChanged: (value) { if (widget.onChanged != null) widget.onChanged!(value); @@ -1204,11 +1208,11 @@ class _CustomTagTextFieldState extends State { suffixIcon: Icon(widget.icon, size: 18.0), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(borderRS), - borderSide: BorderSide(color: Get.theme.colorScheme.onBackground.withAlpha(100), width: 2.0), + borderSide: BorderSide(color: context.theme.colorScheme.onSurface.withAlpha(100), width: 2.0), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(borderR), - borderSide: BorderSide(color: Get.theme.colorScheme.onBackground.withAlpha(100), width: 1.0), + borderSide: BorderSide(color: context.theme.colorScheme.onSurface.withAlpha(100), width: 1.0), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(borderR), @@ -1219,7 +1223,7 @@ class _CustomTagTextFieldState extends State { borderSide: BorderSide(color: Colors.brown.withAlpha(200), width: 2.0), ), hintText: widget.hintText, - hintStyle: context.textTheme.displaySmall?.copyWith(fontSize: 14.5.multipliedFontScale, color: context.textTheme.displaySmall?.color?.withAlpha(120)), + hintStyle: context.textTheme.displaySmall?.copyWith(fontSize: 14.5, color: context.textTheme.displaySmall?.color?.withAlpha(120)), ), ); } diff --git a/lib/ui/dialogs/general_popup_dialog.dart b/lib/ui/dialogs/general_popup_dialog.dart index 06b46bde..2bcdddd3 100644 --- a/lib/ui/dialogs/general_popup_dialog.dart +++ b/lib/ui/dialogs/general_popup_dialog.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:share_plus/share_plus.dart'; import 'package:namida/class/folder.dart'; @@ -70,11 +70,11 @@ Future showGeneralPopupDialog( final tracksExisting = []; if (isSingle || errorPlayingTrack != null) { // -- fill using real-time checks if there was an error. - tracks.loop((t, index) { + tracks.loop((t) { if (File(t.path).existsSync()) tracksExisting.add(t); }); } else { - tracks.loop((t, index) { + tracks.loop((t) { final existingTrack = t.path.toTrackOrNull(); if (existingTrack != null) tracksExisting.add(existingTrack); }); @@ -86,13 +86,13 @@ Future showGeneralPopupDialog( ? tracks[tracks.indexOfImage] : tracks.first; - final colorDelightened = CurrentColor.inst.color.obs; - final iconColor = Color.alphaBlend(colorDelightened.value.withAlpha(120), Get.textTheme.displayMedium!.color!).obs; + final colorDelightened = CurrentColor.inst.color.obso; + final iconColor = Color.alphaBlend(colorDelightened.value.withAlpha(120), namida.textTheme.displayMedium!.color!).obso; if (extractColor && trackToExtractColorFrom != null) { CurrentColor.inst.getTrackDelightnedColor(trackToExtractColorFrom, useIsolate: true).executeWithMinDelay().then((c) { if (c == colorDelightened.value) return; colorDelightened.value = c; - iconColor.value = Color.alphaBlend(c.withAlpha(120), Get.textTheme.displayMedium!.color!); + iconColor.value = Color.alphaBlend(c.withAlpha(120), namida.textTheme.displayMedium!.color!); }); } @@ -106,8 +106,8 @@ Future showGeneralPopupDialog( final Iterable availableYoutubeIDs = tracks.map((e) => YoutubeID(id: e.youtubeID, playlistID: null)).where((element) => element.id != ''); - final numberOfRepeats = 1.obs; - final isLoadingFilesToShare = false.obs; + final numberOfRepeats = 1.obso; + final isLoadingFilesToShare = false.obso; bool shoulShowPlaylistUtils() => tracksWithDates.length > 1 && playlistName != null && !PlaylistController.inst.isOneOfDefaultPlaylists(playlistName); bool shoulShowRemoveFromPlaylist() => tracksWithDates.isNotEmpty && playlistName != null && playlistName != k_PLAYLIST_NAME_MOST_PLAYED; @@ -124,19 +124,18 @@ Future showGeneralPopupDialog( child: Column( children: [ iconWidget ?? - StreamBuilder( - initialData: iconColor.value, - stream: iconColor.stream, - builder: (context, snapshot) => Icon( + ObxO( + rx: iconColor, + builder: (color) => Icon( icon, - color: snapshot.data, + color: color, ), ), if (subtitle != '') ...[ const SizedBox(height: 2.0), Text( subtitle, - style: Get.textTheme.displaySmall?.copyWith(fontSize: 12.0.multipliedFontScale), + style: namida.textTheme.displaySmall?.copyWith(fontSize: 12.0), maxLines: 1, ) ] @@ -150,11 +149,10 @@ Future showGeneralPopupDialog( Future openDialog(Widget widget, {void Function()? onDisposing}) async { await NamidaNavigator.inst.navigateDialog( onDisposing: onDisposing, - dialog: StreamBuilder( - initialData: colorDelightened.value, - stream: colorDelightened.stream, - builder: (context, snapshot) => AnimatedTheme( - data: AppThemes.inst.getAppTheme(snapshot.data, null, true), + dialog: ObxO( + rx: colorDelightened, + builder: (color) => AnimatedTheme( + data: AppThemes.inst.getAppTheme(color, null, true), child: widget, ), ), @@ -183,7 +181,7 @@ Future showGeneralPopupDialog( onPressed: () async { List moodsPre = controller.text.split(','); List moodsFinal = []; - moodsPre.loop((m, index) { + moodsPre.loop((m) { if (!m.contains(',') && m != ' ' && m.isNotEmpty) { moodsFinal.add(m.trimAll()); } @@ -200,7 +198,7 @@ Future showGeneralPopupDialog( children: [ Text( subtitle, - style: Get.textTheme.displaySmall, + style: namida.textTheme.displaySmall, ), const SizedBox( height: 20.0, @@ -347,12 +345,9 @@ Future showGeneralPopupDialog( title: lang.UNDO_CHANGES, message: lang.UNDO_CHANGES_DELETED_PLAYLIST, displaySeconds: 3, - button: TextButton( - onPressed: () async { - await PlaylistController.inst.reAddPlaylist(pl, pl.modifiedDate); - Get.closeAllSnackbars(); - }, - child: Text(lang.UNDO), + button: ( + lang.UNDO, + () async => await PlaylistController.inst.reAddPlaylist(pl, pl.modifiedDate), ), ); } @@ -418,7 +413,7 @@ Future showGeneralPopupDialog( children: [ Text( lang.HIGH_MATCHES, - style: Get.textTheme.displayMedium, + style: namida.textTheme.displayMedium, ), const SizedBox(height: 8.0), ...highMatchesFiles.map( @@ -456,9 +451,8 @@ Future showGeneralPopupDialog( /// Searching final txtc = TextEditingController(); - final RxList filteredPaths = [].obs; - filteredPaths.addAll(paths); - final RxBool shouldCleanUp = true.obs; + final filteredPaths = List.from(paths).obso; + final shouldCleanUp = true.obso; await openDialog( onDisposing: () { @@ -479,8 +473,8 @@ Future showGeneralPopupDialog( ), ], child: SizedBox( - width: Get.width, - height: Get.height * 0.5, + width: namida.width, + height: namida.height * 0.5, child: Column( children: [ Row( @@ -498,10 +492,11 @@ Future showGeneralPopupDialog( }, ), ), - Obx( - () => NamidaIconButton( - tooltip: shouldCleanUp.value ? lang.DISABLE_SEARCH_CLEANUP : lang.ENABLE_SEARCH_CLEANUP, - icon: shouldCleanUp.value ? Broken.shield_cross : Broken.shield_search, + ObxO( + rx: shouldCleanUp, + builder: (cleanup) => NamidaIconButton( + tooltip: cleanup ? lang.DISABLE_SEARCH_CLEANUP : lang.ENABLE_SEARCH_CLEANUP, + icon: cleanup ? Broken.shield_cross : Broken.shield_search, onPressed: () => shouldCleanUp.value = !shouldCleanUp.value, ), ) @@ -509,11 +504,12 @@ Future showGeneralPopupDialog( ), const SizedBox(height: 8.0), Expanded( - child: Obx( - () => NamidaListView( + child: ObxO( + rx: filteredPaths, + builder: (filtered) => NamidaListView( header: highMatchesFiles.isNotEmpty ? highMatchesWidget(highMatchesFiles) : null, itemBuilder: (context, i) { - final p = filteredPaths[i]; + final p = filtered[i]; return SmallListTile( key: ValueKey(i), borderRadius: 12.0, @@ -522,8 +518,8 @@ Future showGeneralPopupDialog( onTap: () => updatePathDialog(p), ); }, - itemCount: filteredPaths.length, - itemExtents: null, + itemCount: filtered.length, + itemExtent: null, ), ), ), @@ -543,9 +539,10 @@ Future showGeneralPopupDialog( NamidaLinkUtils.openLink(link); } - final advancedStuffListTile = Obx( - () => SmallListTile( - color: colorDelightened.value, + final advancedStuffListTile = ObxO( + rx: colorDelightened, + builder: (colorDelightened) => SmallListTile( + color: colorDelightened, compact: false, title: lang.ADVANCED, icon: Broken.code_circle, @@ -553,7 +550,7 @@ Future showGeneralPopupDialog( cancelSkipTimer(); showTrackAdvancedDialog( tracks: tracksWithDates.isNotEmpty ? tracksWithDates : tracks, - colorScheme: colorDelightened.value, + colorScheme: colorDelightened, source: source, albumsUniqued: availableAlbums, ); @@ -562,9 +559,10 @@ Future showGeneralPopupDialog( ); final Widget? removeFromPlaylistListTile = shoulShowRemoveFromPlaylist() - ? Obx( - () => SmallListTile( - color: colorDelightened.value, + ? ObxO( + rx: colorDelightened, + builder: (colorDelightened) => SmallListTile( + color: colorDelightened, compact: true, title: lang.REMOVE_FROM_PLAYLIST, subtitle: playlistName!.translatePlaylistName(), @@ -611,9 +609,10 @@ Future showGeneralPopupDialog( ) : null; final Widget? removeQueueTile = queue != null - ? Obx( - () => SmallListTile( - color: colorDelightened.value, + ? ObxO( + rx: colorDelightened, + builder: (colorDelightened) => SmallListTile( + color: colorDelightened, compact: false, title: lang.REMOVE_QUEUE, icon: Broken.pen_remove, @@ -638,12 +637,11 @@ Future showGeneralPopupDialog( durationInMs: 400, scale: 0.92, onDismissing: cancelSkipTimer, - dialog: StreamBuilder( - initialData: colorDelightened.value, - stream: colorDelightened.stream, - builder: (context, snapshot) { - final theme = AppThemes.inst.getAppTheme(snapshot.data, null, false); - final iconColor = Color.alphaBlend(colorDelightened.value.withAlpha(120), theme.textTheme.displayMedium!.color!); + dialog: ObxO( + rx: colorDelightened, + builder: (colorDelightened) { + final theme = AppThemes.inst.getAppTheme(colorDelightened, null, false); + final iconColor = Color.alphaBlend(colorDelightened.withAlpha(120), theme.textTheme.displayMedium!.color!); return AnimatedTheme( data: theme, child: Dialog( @@ -662,7 +660,7 @@ Future showGeneralPopupDialog( false, comingFromQueue: comingFromQueue, index: index, - colorScheme: colorDelightened.value, + colorScheme: colorDelightened, queueSource: source, additionalHero: additionalHero, ) @@ -707,8 +705,8 @@ Future showGeneralPopupDialog( overflow: TextOverflow.ellipsis, maxLines: 1, style: theme.textTheme.displayLarge?.copyWith( - fontSize: 17.0.multipliedFontScale, - color: Color.alphaBlend(colorDelightened.value.withAlpha(40), theme.textTheme.displayMedium!.color!), + fontSize: 17.0, + color: Color.alphaBlend(colorDelightened.withAlpha(40), theme.textTheme.displayMedium!.color!), ), ), const SizedBox( @@ -720,8 +718,8 @@ Future showGeneralPopupDialog( overflow: TextOverflow.ellipsis, maxLines: 1, style: theme.textTheme.displayMedium?.copyWith( - fontSize: 14.0.multipliedFontScale, - color: Color.alphaBlend(colorDelightened.value.withAlpha(80), theme.textTheme.displayMedium!.color!), + fontSize: 14.0, + color: Color.alphaBlend(colorDelightened.withAlpha(80), theme.textTheme.displayMedium!.color!), ), ), if (thirdLineText.isNotEmpty) ...[ @@ -733,8 +731,8 @@ Future showGeneralPopupDialog( overflow: TextOverflow.ellipsis, maxLines: 1, style: theme.textTheme.displaySmall?.copyWith( - fontSize: 12.5.multipliedFontScale, - color: Color.alphaBlend(colorDelightened.value.withAlpha(40), theme.textTheme.displayMedium!.color!), + fontSize: 12.5, + color: Color.alphaBlend(colorDelightened.withAlpha(40), theme.textTheme.displayMedium!.color!), ), ), ] @@ -782,18 +780,19 @@ Future showGeneralPopupDialog( SmallListTile( title: lang.UPDATE, subtitle: tracks.first.path, - color: colorDelightened.value, + color: colorDelightened, compact: true, icon: Broken.document_upload, onTap: () async { cancelSkipTimer(); NamidaNavigator.inst.closeDialog(); - if (Indexer.inst.allAudioFiles.isEmpty) { + if (Indexer.inst.allAudioFiles.value.isEmpty) { await Indexer.inst.getAudioFiles(); } /// firstly checks if a file exists in current library - final firstHighMatchesFiles = NamidaGenerator.getHighMatcheFilesFromFilename(Indexer.inst.allAudioFiles, tracks.first.path.getFilename).toSet(); + final firstHighMatchesFiles = + NamidaGenerator.getHighMatcheFilesFromFilename(Indexer.inst.allAudioFiles.value, tracks.first.path.getFilename).toSet(); if (firstHighMatchesFiles.isNotEmpty) { await openDialog( CustomBlurryDialog( @@ -817,29 +816,27 @@ Future showGeneralPopupDialog( }, ), if (errorPlayingTrack != null) - Obx( - () { - final remainingSecondsToSkip = Player.inst.playErrorRemainingSecondsToSkip; - return SmallListTile( - title: lang.SKIP, - subtitle: remainingSecondsToSkip <= 0 ? null : '$remainingSecondsToSkip ${lang.SECONDS}', - color: colorDelightened.value, - compact: true, - icon: Broken.next, - trailing: remainingSecondsToSkip <= 0 - ? null - : NamidaIconButton( - icon: Broken.close_circle, - iconColor: Get.context?.defaultIconColor(colorDelightened.value, theme.textTheme.displayMedium?.color), - onPressed: cancelSkipTimer, - ), - onTap: () { - cancelSkipTimer(); - NamidaNavigator.inst.closeDialog(); - Player.inst.next(); - }, - ); - }, + ObxO( + rx: Player.inst.playErrorRemainingSecondsToSkip, + builder: (remainingSecondsToSkip) => SmallListTile( + title: lang.SKIP, + subtitle: remainingSecondsToSkip <= 0 ? null : '$remainingSecondsToSkip ${lang.SECONDS}', + color: colorDelightened, + compact: true, + icon: Broken.next, + trailing: remainingSecondsToSkip <= 0 + ? null + : NamidaIconButton( + icon: Broken.close_circle, + iconColor: namida.context?.defaultIconColor(colorDelightened, theme.textTheme.displayMedium?.color), + onPressed: cancelSkipTimer, + ), + onTap: () { + cancelSkipTimer(); + NamidaNavigator.inst.closeDialog(); + Player.inst.next(); + }, + ), ), ], advancedStuffListTile, @@ -857,7 +854,7 @@ Future showGeneralPopupDialog( children: [ if (availableAlbums.length == 1 && albumToAddFrom == null) SmallListTile( - color: colorDelightened.value, + color: colorDelightened, compact: true, title: lang.GO_TO_ALBUM, subtitle: availableAlbums.first.$1, @@ -875,7 +872,7 @@ Future showGeneralPopupDialog( ), if (availableAlbums.length == 1 && albumToAddFrom != null) SmallListTile( - color: colorDelightened.value, + color: colorDelightened, compact: true, title: lang.ADD_MORE_FROM_TO_QUEUE.replaceFirst('_MEDIA_', '"${albumToAddFrom.$1}"'), icon: Broken.music_dashboard, @@ -896,7 +893,7 @@ Future showGeneralPopupDialog( icon: Broken.music_dashboard, iconColor: iconColor, titleText: lang.GO_TO_ALBUM, - textColorScheme: colorDelightened.value, + textColorScheme: colorDelightened, childrenPadding: const EdgeInsets.only(left: 20.0, right: 20.0, bottom: 12.0, top: 0), children: [ Wrap( @@ -915,7 +912,7 @@ Future showGeneralPopupDialog( ), if (artistToAddFrom != null) SmallListTile( - color: colorDelightened.value, + color: colorDelightened, compact: true, title: lang.ADD_MORE_FROM_TO_QUEUE.replaceFirst('_MEDIA_', '"$artistToAddFrom"'), icon: Broken.microphone, @@ -933,7 +930,7 @@ Future showGeneralPopupDialog( ), if (artistToAddFrom == null && availableArtists.length == 1) SmallListTile( - color: colorDelightened.value, + color: colorDelightened, compact: true, title: lang.GO_TO_ARTIST, subtitle: availableArtists.first, @@ -954,7 +951,7 @@ Future showGeneralPopupDialog( icon: Broken.profile_2user, iconColor: iconColor, titleText: lang.GO_TO_ARTIST, - textColorScheme: colorDelightened.value, + textColorScheme: colorDelightened, childrenPadding: const EdgeInsets.only(left: 20.0, right: 20.0, bottom: 12.0, top: 0), children: [ Wrap( @@ -975,7 +972,7 @@ Future showGeneralPopupDialog( /// Folders if (availableFolders.length == 1) SmallListTile( - color: colorDelightened.value, + color: colorDelightened, compact: true, title: lang.GO_TO_FOLDER, subtitle: availableFolders.first.folderName, @@ -988,7 +985,7 @@ Future showGeneralPopupDialog( trailing: IconButton( tooltip: lang.ADD_MORE_FROM_THIS_FOLDER, onPressed: () { - final tracks = availableFolders.first.tracks; + final tracks = availableFolders.first.tracks(); Player.inst.addToQueue(tracks, insertNext: true, insertionType: QueueInsertionType.moreFolder); }, icon: const Icon(Broken.add), @@ -996,11 +993,14 @@ Future showGeneralPopupDialog( ), SmallListTile( - color: colorDelightened.value, + color: colorDelightened, compact: false, title: lang.SHARE, icon: Broken.share, - trailing: Obx(() => isLoadingFilesToShare.value ? const LoadingIndicator() : const SizedBox()), + trailing: ObxO( + rx: isLoadingFilesToShare, + builder: (loading) => loading ? const LoadingIndicator() : const SizedBox(), + ), onTap: () async { isLoadingFilesToShare.value = true; await Share.shareXFiles(tracksExisting.mapped((e) => XFile(e.path))); @@ -1009,25 +1009,25 @@ Future showGeneralPopupDialog( }, ), - isSingle && tracks.first == Player.inst.nowPlayingTrack + isSingle && tracks.first == Player.inst.currentItem.value ? NamidaOpacity( - opacity: Player.inst.sleepAfterTracks == 1 ? 0.6 : 1.0, + opacity: Player.inst.sleepTimerConfig.value.sleepAfterItems == 1 ? 0.6 : 1.0, child: IgnorePointer( - ignoring: Player.inst.sleepAfterTracks == 1, + ignoring: Player.inst.sleepTimerConfig.value.sleepAfterItems == 1, child: SmallListTile( - color: colorDelightened.value, + color: colorDelightened, compact: false, title: lang.STOP_AFTER_THIS_TRACK, icon: Broken.pause, onTap: () { NamidaNavigator.inst.closeDialog(); - Player.inst.updateSleepTimerValues(enableSleepAfterTracks: true, sleepAfterTracks: 1); + Player.inst.updateSleepTimerValues(enableSleepAfterItems: true, sleepAfterItems: 1); }, ), ), ) : SmallListTile( - color: colorDelightened.value, + color: colorDelightened, compact: false, title: isSingle ? lang.PLAY : lang.PLAY_ALL, icon: Broken.play, @@ -1039,7 +1039,7 @@ Future showGeneralPopupDialog( if (!isSingle) SmallListTile( - color: colorDelightened.value, + color: colorDelightened, compact: false, title: lang.SHUFFLE, icon: Broken.shuffle, @@ -1050,7 +1050,7 @@ Future showGeneralPopupDialog( ), SmallListTile( - color: colorDelightened.value, + color: colorDelightened, compact: false, title: lang.ADD_TO_PLAYLIST, icon: Broken.music_library_2, @@ -1060,13 +1060,13 @@ Future showGeneralPopupDialog( }, ), SmallListTile( - color: colorDelightened.value, + color: colorDelightened, compact: false, title: lang.EDIT_TAGS, icon: Broken.edit, onTap: () { NamidaNavigator.inst.closeDialog(); - showEditTracksTagsDialog(tracks, colorDelightened.value); + showEditTracksTagsDialog(tracks, colorDelightened); }, trailing: isSingle ? IconButton( @@ -1085,7 +1085,7 @@ Future showGeneralPopupDialog( color: iconColor, ), iconSize: 20.0, - onPressed: () => showLRCSetDialog(tracks.first, colorDelightened.value), + onPressed: () => showLRCSetDialog(tracks.first, colorDelightened), ) : null, ), @@ -1094,7 +1094,7 @@ Future showGeneralPopupDialog( if (availableYoutubeIDs.isNotEmpty) SmallListTile( - color: colorDelightened.value, + color: colorDelightened, compact: true, title: lang.OPEN_IN_YOUTUBE_VIEW, icon: Broken.video, @@ -1106,13 +1106,13 @@ Future showGeneralPopupDialog( if (removeQueueTile != null) removeQueueTile, - if (Player.inst.currentQueue.isNotEmpty && Player.inst.latestInsertedIndex != Player.inst.currentIndex) + if (Player.inst.currentItem.value is Selectable && Player.inst.latestInsertedIndex != Player.inst.currentIndex.value) () { - final playAfterTrack = Player.inst.currentQueue.elementAt(Player.inst.latestInsertedIndex).track; + final playAfterTrack = (Player.inst.currentQueue.value[Player.inst.latestInsertedIndex] as Selectable).track; return SmallListTile( - color: colorDelightened.value, + color: colorDelightened, compact: true, - title: '${lang.PLAY_AFTER}: ${(Player.inst.latestInsertedIndex - Player.inst.currentIndex).displayTrackKeyword}', + title: '${lang.PLAY_AFTER}: ${(Player.inst.latestInsertedIndex - Player.inst.currentIndex.value).displayTrackKeyword}', subtitle: [playAfterTrack.artistsList.firstOrNull, playAfterTrack.title].joinText(separator: ' - '), icon: Broken.hierarchy_square, onTap: () { @@ -1122,17 +1122,18 @@ Future showGeneralPopupDialog( ); }(), - if (isSingle && tracks.first == Player.inst.nowPlayingTrack) - Obx( - () => SmallListTile( - color: colorDelightened.value, + if (isSingle && tracks.first == Player.inst.currentTrack?.track) + ObxO( + rx: numberOfRepeats, + builder: (repeats) => SmallListTile( + color: colorDelightened, compact: true, - title: lang.REPEAT_FOR_N_TIMES.replaceFirst('_NUM_', numberOfRepeats.value.toString()), + title: lang.REPEAT_FOR_N_TIMES.replaceFirst('_NUM_', repeats.toString()), icon: Broken.cd, onTap: () { NamidaNavigator.inst.closeDialog(); settings.player.save(repeatMode: RepeatMode.forNtimes); - Player.inst.updateNumberOfRepeats(numberOfRepeats.value); + Player.inst.updateNumberOfRepeats(repeats); }, trailing: Row( mainAxisSize: MainAxisSize.min, @@ -1174,7 +1175,7 @@ Future showGeneralPopupDialog( Broken.grammerly, lang.SET_RATING, setTrackRating, - subtitle: stats == null || stats.value.rating == 0 ? '' : ' ${stats.value.rating}%', + subtitle: stats == null || stats.valueR.rating == 0 ? '' : ' ${stats.valueR.rating}%', ), ), ), @@ -1184,7 +1185,7 @@ Future showGeneralPopupDialog( child: bigIcon( Broken.edit_2, lang.SET_YOUTUBE_LINK, - () => showSetYTLinkCommentDialog(tracks, colorDelightened.value), + () => showSetYTLinkCommentDialog(tracks, colorDelightened), iconWidget: StackedIcon( baseIcon: Broken.edit_2, secondaryIcon: Broken.video_square, @@ -1218,7 +1219,7 @@ Future showGeneralPopupDialog( children: [ Expanded( child: SmallListTile( - color: colorDelightened.value, + color: colorDelightened, compact: false, title: lang.PLAY_NEXT, icon: Broken.next, @@ -1235,7 +1236,7 @@ Future showGeneralPopupDialog( ), Expanded( child: SmallListTile( - color: colorDelightened.value, + color: colorDelightened, compact: false, title: lang.PLAY_LAST, icon: Broken.play_cricle, @@ -1283,7 +1284,7 @@ class _SmallUnderlinedChip extends StatelessWidget { text, style: textTheme.displaySmall?.copyWith( decoration: TextDecoration.underline, - fontSize: 13.5.multipliedFontScale, + fontSize: 13.5, ), ), ), diff --git a/lib/ui/dialogs/set_lrc_dialog.dart b/lib/ui/dialogs/set_lrc_dialog.dart index 85dfe6a9..4c9ac9fc 100644 --- a/lib/ui/dialogs/set_lrc_dialog.dart +++ b/lib/ui/dialogs/set_lrc_dialog.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; import 'package:lrc/lrc.dart'; import 'package:namida/class/lyrics.dart'; @@ -18,11 +17,17 @@ import 'package:namida/controller/player_controller.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/packages/three_arched_circle.dart'; import 'package:namida/ui/dialogs/edit_tags_dialog.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; +import 'package:namida/youtube/class/youtube_id.dart'; -void showLRCSetDialog(Track trackPre, Color colorScheme) async { +void showLRCSetDialog(Playable item, Color colorScheme) async { + if (item is YoutubeID) return; // TODO: allow lyrics for youtube videos + if (item is! Selectable) return; + + final trackPre = item.track; final track = trackPre.toTrackExt(); final fetchingFromInternet = Rxn(); final availableLyrics = [].obs; @@ -85,7 +90,7 @@ void showLRCSetDialog(Track trackPre, Color colorScheme) async { } void updateForCurrentTrack() { - if (trackPre == Player.inst.nowPlayingTrack) { + if (trackPre == Player.inst.currentItem.value) { Lyrics.inst.updateLyrics(trackPre); } } @@ -229,16 +234,16 @@ void showLRCSetDialog(Track trackPre, Color colorScheme) async { children: [ Text( lang.OFFSET, - style: Get.textTheme.displayMedium, + style: namida.textTheme.displayMedium, ), Obx( () { - final ms = newOffset.value.remainder(1000).abs().toString(); + final ms = newOffset.valueR.remainder(1000).abs().toString(); final msText = ms.length > 2 ? ms.substring(0, 2) : ms; - final off = newOffset.value; + final off = newOffset.valueR; return Text( "${off.milliSecondsLabel}.$msText", - style: Get.textTheme.displaySmall, + style: namida.textTheme.displaySmall, ); }, ), @@ -250,8 +255,8 @@ void showLRCSetDialog(Track trackPre, Color colorScheme) async { const SizedBox(width: 8.0), Obx( () => Text( - "${newOffset.value}ms", - style: Get.textTheme.displayMedium, + "${newOffset.valueR}ms", + style: namida.textTheme.displayMedium, ), ), const SizedBox(width: 8.0), @@ -314,7 +319,7 @@ void showLRCSetDialog(Track trackPre, Color colorScheme) async { const SizedBox(width: 6.0), Obx( () { - final selected = selectedLyrics.value; + final selected = selectedLyrics.valueR; return NamidaButton( enabled: selected != null && !selected.isInCache && !selected.isEmbedded /* && (selected.file != null || selected.fromInternet == true) */, text: lang.SAVE, @@ -331,8 +336,8 @@ void showLRCSetDialog(Track trackPre, Color colorScheme) async { ) ], child: SizedBox( - width: Get.width, - height: Get.height * 0.6, + width: namida.width, + height: namida.height * 0.6, child: Column( children: [ Row( @@ -362,14 +367,14 @@ void showLRCSetDialog(Track trackPre, Color colorScheme) async { Expanded( child: Obx( () { - if (fetchingFromInternet.value == true) { + if (fetchingFromInternet.valueR == true) { return ThreeArchedCircle( - color: Get.theme.cardColor, + color: namida.theme.cardColor, size: 58.0, ); } - final both = [...availableLyrics, ...fetchedLyrics]; - if (both.isEmpty && fetchingFromInternet.value != null) { + final both = [...availableLyrics.valueR, ...fetchedLyrics.valueR]; + if (both.isEmpty && fetchingFromInternet.valueR != null) { return const Icon( Broken.emoji_sad, size: 48.0, @@ -391,9 +396,9 @@ void showLRCSetDialog(Track trackPre, Color colorScheme) async { borderRadius: 12.0, animationDurationMS: 200, onTap: () => selectedLyrics.value = l, - bgColor: Get.theme.cardColor.withOpacity(0.4), + bgColor: namida.theme.cardColor.withOpacity(0.4), decoration: BoxDecoration( - border: selectedLyrics.value == l + border: selectedLyrics.valueR == l ? Border.all( width: 2.0, color: colorScheme, @@ -412,7 +417,7 @@ void showLRCSetDialog(Track trackPre, Color colorScheme) async { Expanded( child: Text( cacheText != '' ? "$syncedText ($cacheText)" : syncedText, - style: Get.textTheme.displayMedium, + style: namida.textTheme.displayMedium, ), ), NamidaIconButton( @@ -463,18 +468,18 @@ void showLRCSetDialog(Track trackPre, Color colorScheme) async { children: [ NamidaInkWell( borderRadius: 8.0, - bgColor: Get.theme.cardColor, + bgColor: namida.theme.cardColor, padding: const EdgeInsets.all(8.0), - child: expandedLyrics.value == l + child: expandedLyrics.valueR == l ? Text( l.lyrics, - style: Get.textTheme.displaySmall, + style: namida.textTheme.displaySmall, ) : Text( l.lyrics, maxLines: 12, overflow: TextOverflow.fade, - style: Get.textTheme.displaySmall, + style: namida.textTheme.displaySmall, ), ), Positioned( @@ -488,7 +493,7 @@ void showLRCSetDialog(Track trackPre, Color colorScheme) async { boxShadow: [ BoxShadow( blurRadius: 4.0, - color: Get.theme.scaffoldBackgroundColor, + color: namida.theme.scaffoldBackgroundColor, ), ], ), diff --git a/lib/ui/dialogs/setting_dialog_with_text_field.dart b/lib/ui/dialogs/setting_dialog_with_text_field.dart index 1b9d546e..8bd4692a 100644 --- a/lib/ui/dialogs/setting_dialog_with_text_field.dart +++ b/lib/ui/dialogs/setting_dialog_with_text_field.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - import 'package:namida/controller/navigator_controller.dart'; import 'package:namida/controller/player_controller.dart'; import 'package:namida/controller/playlist_controller.dart'; @@ -10,13 +8,13 @@ import 'package:namida/core/dimensions.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/library/track_tile.dart'; Future showSettingDialogWithTextField({ Widget? topWidget, String title = '', - Widget? iconWidgets, IconData? icon, bool trackThumbnailSizeinList = false, bool trackListTileHeight = false, @@ -38,14 +36,12 @@ Future showSettingDialogWithTextField({ String message, { String? title, Duration? duration, - Widget? iconWidget, }) { snackyy( title: title ?? '', message: "${lang.RESET_TO_DEFAULT}: $message", animationDurationMS: 400, icon: icon, - iconWidget: iconWidget, ); } @@ -167,7 +163,7 @@ Future showSettingDialogWithTextField({ Padding( padding: const EdgeInsets.only(top: 14.0), child: TextFormField( - style: Get.textTheme.displaySmall?.copyWith(fontSize: 16.0.multipliedFontScale, fontWeight: FontWeight.w600), + style: namida.textTheme.displaySmall?.copyWith(fontSize: 16.0, fontWeight: FontWeight.w600), autofocus: true, keyboardType: dateTimeFormat || trackTileSeparator || addNewPlaylist ? TextInputType.text : TextInputType.number, controller: controller, @@ -177,11 +173,11 @@ Future showSettingDialogWithTextField({ errorMaxLines: 3, focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(14.0.multipliedRadius), - borderSide: BorderSide(color: Get.theme.colorScheme.onBackground.withAlpha(100), width: 2.0), + borderSide: BorderSide(color: namida.theme.colorScheme.onSurface.withAlpha(100), width: 2.0), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16.0.multipliedRadius), - borderSide: BorderSide(color: Get.theme.colorScheme.onBackground.withAlpha(100), width: 1.0), + borderSide: BorderSide(color: namida.theme.colorScheme.onSurface.withAlpha(100), width: 1.0), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16.0.multipliedRadius), diff --git a/lib/ui/dialogs/track_advanced_dialog.dart b/lib/ui/dialogs/track_advanced_dialog.dart index aea57380..964759b8 100644 --- a/lib/ui/dialogs/track_advanced_dialog.dart +++ b/lib/ui/dialogs/track_advanced_dialog.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'dart:isolate'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/base/ports_provider.dart'; import 'package:namida/class/color_m.dart'; @@ -22,6 +21,7 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/dialogs/track_clear_dialog.dart'; import 'package:namida/ui/widgets/animated_widgets.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; @@ -40,13 +40,13 @@ void showTrackAdvancedDialog({ final canShowClearDialog = tracks.hasAnythingCached; final Map sourcesMap = {}; - tracks.loop((e, index) { + tracks.loop((e) { final twd = e.trackWithDate; if (twd != null) { sourcesMap.update(twd.source, (value) => value + 1, ifAbsent: () => 1); } }); - final RxBool willUpdateArtwork = false.obs; + final willUpdateArtwork = false.obs; final trackColor = await CurrentColor.inst.getTrackColors(tracks.first.track, delightnedAndAlpha: false); @@ -128,7 +128,7 @@ void showTrackAdvancedDialog({ NamidaButton( text: lang.CONFIRM, onPressed: () async { - final success = await NamidaChannel.inst.setMusicAs(path: tracks.first.track.path, types: selected); + final success = await NamidaChannel.inst.setMusicAs(path: tracks.first.track.path, types: selected.value); if (success) NamidaNavigator.inst.closeDialog(); }, ), @@ -153,20 +153,20 @@ void showTrackAdvancedDialog({ ), Obx( () { - final shouldShow = shouldShowReIndexProgress.value; - final errors = reIndexedTracksFailed.value; + final shouldShow = shouldShowReIndexProgress.valueR; + final errors = reIndexedTracksFailed.valueR; final secondLine = errors > 0 ? '\n${lang.ERROR}: $errors' : ''; return CustomListTile( - enabled: shouldReIndexEnabled.value, + enabled: shouldReIndexEnabled.valueR, passedColor: colorScheme, title: lang.RE_INDEX, icon: Broken.direct_inbox, - subtitle: shouldShow ? "${reIndexedTracksSuccessful.value}/${tracksUniqued.length}$secondLine" : null, + subtitle: shouldShow ? "${reIndexedTracksSuccessful.valueR}/${tracksUniqued.length}$secondLine" : null, trailingRaw: NamidaInkWell( padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 12.0), bgColor: theme.cardColor, - onTap: () => willUpdateArtwork.value = !willUpdateArtwork.value, - child: Obx(() => Text('${lang.ARTWORK} ${willUpdateArtwork.value ? '✓' : 'x'}')), + onTap: () => willUpdateArtwork.toggle(), + child: Obx(() => Text('${lang.ARTWORK} ${willUpdateArtwork.valueR ? '✓' : 'x'}')), ), onTap: () async { await Indexer.inst.reindexTracks( @@ -285,7 +285,7 @@ void _showTrackColorPaletteDialog({ NamidaNavigator.inst.navigateDialog( colorScheme: colorScheme, dialogBuilder: (theme) => NamidaColorPickerDialog( - initialColor: allPaletteColor.lastOrNull ?? Colors.black, + initialColor: allPaletteColor.value.lastOrNull ?? Colors.black, doneText: lang.ADD, onColorChanged: (value) => color = value, onDonePressed: () { @@ -303,7 +303,7 @@ void _showTrackColorPaletteDialog({ final finalColorToBeUsed = trackColor.color.obs; Widget getText(String text, {TextStyle? style}) { - return Text(text, style: style ?? Get.textTheme.displaySmall); + return Text(text, style: style ?? namida.textTheme.displaySmall); } Widget getColorWidget(Color? color, [Widget? child]) => CircleAvatar( @@ -334,7 +334,7 @@ void _showTrackColorPaletteDialog({ alignment: Alignment.centerLeft, padding: const EdgeInsets.all(6.0), decoration: BoxDecoration( - color: (Get.isDarkMode ? Colors.black : Colors.white).withAlpha(160), + color: (namida.isDarkMode ? Colors.black : Colors.white).withAlpha(160), border: Border.all(color: theme.shadowColor), borderRadius: BorderRadius.circular(12.0.multipliedRadius), ), @@ -394,7 +394,7 @@ void _showTrackColorPaletteDialog({ const CancelButton(), NamidaButton( text: lang.CONFIRM, - onPressed: () => onFinalColor(allPaletteColor, finalColorToBeUsed.value), + onPressed: () => onFinalColor(allPaletteColor.value, finalColorToBeUsed.value), ), ], child: Column( @@ -413,7 +413,7 @@ void _showTrackColorPaletteDialog({ getText(lang.REMOVED), const SizedBox(height: 8.0), getPalettesWidget( - palette: removedColors, + palette: removedColors.valueR, onColorTap: (color) {}, onColorLongPress: (color) { allPaletteColor.add(color); @@ -433,7 +433,7 @@ void _showTrackColorPaletteDialog({ const SizedBox(height: 8.0), Obx( () => getPalettesWidget( - palette: allPaletteColor, + palette: allPaletteColor.valueR, onColorTap: (color) => selectedColors.addOrRemove(color), onColorLongPress: (color) { allPaletteColor.remove(color); @@ -469,15 +469,15 @@ void _showTrackColorPaletteDialog({ title: lang.PALETTE_MIX, colors: trackColor.palette, ), - if (didChangeOriginalPalette.value) + if (didChangeOriginalPalette.valueR) mixWidget( title: lang.PALETTE_NEW_MIX, - colors: allPaletteColor, + colors: allPaletteColor.valueR, ), if (selectedColors.isNotEmpty) mixWidget( title: lang.PALETTE_SELECTED_MIX, - colors: selectedColors, + colors: selectedColors.valueR, ), ], ), @@ -488,14 +488,15 @@ void _showTrackColorPaletteDialog({ ), Row( children: [ - getText('${lang.USED} : ', style: Get.textTheme.displayMedium), + getText('${lang.USED} : ', style: namida.textTheme.displayMedium), const SizedBox(width: 12.0), Expanded( - child: Obx( - () => AnimatedSizedBox( + child: ObxO( + rx: finalColorToBeUsed, + builder: (finalColorToBeUsed) => AnimatedSizedBox( duration: const Duration(milliseconds: 200), decoration: BoxDecoration( - color: finalColorToBeUsed.value, + color: finalColorToBeUsed, borderRadius: BorderRadius.circular(8.0.multipliedRadius), ), width: double.infinity, @@ -558,17 +559,18 @@ void showLibraryTracksChooseDialog({ insetPadding: const EdgeInsets.all(32.0), actions: [ const CancelButton(), - Obx( - () => NamidaButton( - enabled: selectedTrack.value != null, + ObxO( + rx: selectedTrack, + builder: (selectedTr) => NamidaButton( + enabled: selectedTr != null, text: lang.CONFIRM, onPressed: () => onChoose(selectedTrack.value!), ), ) ], child: SizedBox( - width: Get.width, - height: Get.height * 0.7, + width: namida.width, + height: namida.height * 0.7, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -603,8 +605,9 @@ void showLibraryTracksChooseDialog({ searchController.clear(); }, ), - Obx( - () => isSearching.value + ObxO( + rx: isSearching, + builder: (isSearching) => isSearching ? const CircularProgressIndicator.adaptive( strokeWidth: 2.0, strokeCap: StrokeCap.round, @@ -622,7 +625,7 @@ void showLibraryTracksChooseDialog({ padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Text( trackName, - style: Get.textTheme.displayMedium, + style: namida.textTheme.displayMedium, ), ), if (trackName != '') const SizedBox(height: 8.0), @@ -636,17 +639,18 @@ void showLibraryTracksChooseDialog({ itemCount: allTracksList.length, itemExtent: Dimensions.inst.trackTileItemExtent, itemBuilder: (context, i) { - final tr = allTracksList[i]; - return Obx( - () => TrackTile( - trackOrTwd: tr, - index: i, - queueSource: QueueSource.playlist, - onTap: () => onTrackTap(tr), - onRightAreaTap: () => onTrackTap(tr), - trailingWidget: NamidaCheckMark( + final tr = allTracksList.value[i]; + return TrackTile( + trackOrTwd: tr, + index: i, + queueSource: QueueSource.playlist, + onTap: () => onTrackTap(tr), + onRightAreaTap: () => onTrackTap(tr), + trailingWidget: ObxO( + rx: selectedTrack, + builder: (selectedTrack) => NamidaCheckMark( size: 22.0, - active: selectedTrack.value == tr, + active: selectedTrack == tr, ), ), ); diff --git a/lib/ui/dialogs/track_info_dialog.dart b/lib/ui/dialogs/track_info_dialog.dart index 2f06abbc..c6b5bcfd 100644 --- a/lib/ui/dialogs/track_info_dialog.dart +++ b/lib/ui/dialogs/track_info_dialog.dart @@ -2,14 +2,13 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; import 'package:jiffy/jiffy.dart'; import 'package:just_audio/just_audio.dart'; import 'package:photo_view/photo_view.dart'; +import 'package:namida/base/audio_handler.dart'; import 'package:namida/class/split_config.dart'; import 'package:namida/class/track.dart'; -import 'package:namida/base/audio_handler.dart'; import 'package:namida/controller/current_color.dart'; import 'package:namida/controller/edit_delete_controller.dart'; import 'package:namida/controller/history_controller.dart'; @@ -23,6 +22,7 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/themes.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/dialogs/common_dialogs.dart'; import 'package:namida/ui/dialogs/track_listens_dialog.dart'; import 'package:namida/ui/widgets/artwork.dart'; @@ -52,7 +52,7 @@ Future showTrackInfoDialog( totalListens.sortByReverse((e) => e); final firstListenTrack = totalListens.lastOrNull; - final color = (colorScheme ?? CurrentColor.inst.color).obs; + final color = (colorScheme ?? CurrentColor.inst.color).obso; if (colorScheme == null) { CurrentColor.inst.getTrackDelightnedColor(track, useIsolate: true).executeWithMinDelay(delayMS: 100).then((c) { @@ -64,7 +64,7 @@ Future showTrackInfoDialog( bool shouldShowTheField(bool isUnknown) => !isUnknown || (settings.showUnknownFieldsInTrackInfoDialog.value && isUnknown); void showPreviewTrackDialog() async { - final wasPlaying = Player.inst.isPlaying; + final wasPlaying = Player.inst.isPlaying.value; if (wasPlaying) { Player.inst.pause(); } @@ -81,57 +81,54 @@ Future showTrackInfoDialog( Player.inst.play(); } }, - dialog: StreamBuilder( - initialData: color.value, - stream: color.stream, - builder: (context, snapshot) { - final theme = AppThemes.inst.getAppTheme(snapshot.data, null, true); + dialog: ObxO( + rx: color, + builder: (dialogColor) { + final theme = AppThemes.inst.getAppTheme(dialogColor, null, true); return AnimatedTheme( data: theme, - child: Builder(builder: (context) { - return CustomBlurryDialog( - theme: theme, - insetPadding: const EdgeInsets.all(24.0), - title: lang.PREVIEW, - normalTitleStyle: true, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - StreamBuilder( - stream: ap.positionStream, - builder: (context, snapshot) { - final dur = snapshot.data ?? Duration.zero; - return Text(dur.inSeconds.secondsLabel); - }, - ), - StreamBuilder( - initialData: Duration.zero, - stream: ap.positionStream, - builder: (context, snapshot) { - final dur = snapshot.data ?? Duration.zero; - return Slider.adaptive( - value: dur.inMilliseconds.toDouble(), - min: 0, - max: ap.duration?.inMilliseconds.toDouble() ?? 0, - onChanged: (value) => ap.seek(Duration(milliseconds: value.toInt())), - ); - }, - ), - Text(((ap.duration?.inSeconds ?? 0).secondsLabel)), - StreamBuilder( - stream: ap.playingStream, - builder: (context, snapshot) { - final isPlaying = snapshot.data ?? false; - return NamidaIconButton( - icon: isPlaying ? Broken.pause : Broken.play, - onPressed: ap.playing ? ap.pause : ap.play, - ); - }, - ), - ], - ), - ); - }), + child: CustomBlurryDialog( + theme: theme, + insetPadding: const EdgeInsets.all(24.0), + title: lang.PREVIEW, + normalTitleStyle: true, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + StreamBuilder( + stream: ap.positionStream, + builder: (context, snapshot) { + final dur = snapshot.data ?? Duration.zero; + return Text(dur.inSeconds.secondsLabel); + }, + ), + StreamBuilder( + initialData: Duration.zero, + stream: ap.positionStream, + builder: (context, snapshot) { + final dur = snapshot.data ?? Duration.zero; + return Slider.adaptive( + value: dur.inMilliseconds.toDouble(), + min: 0, + max: ap.duration?.inMilliseconds.toDouble() ?? 0, + onChanged: (value) => ap.seek(Duration(milliseconds: value.toInt())), + ); + }, + ), + Text(((ap.duration?.inSeconds ?? 0).secondsLabel)), + StreamBuilder( + stream: ap.playingStream, + builder: (context, snapshot) { + final isPlaying = snapshot.data ?? false; + return NamidaIconButton( + icon: isPlaying ? Broken.pause : Broken.play, + onPressed: ap.playing ? ap.pause : ap.play, + ); + }, + ), + ], + ), + ), ); }, ), @@ -163,11 +160,10 @@ Future showTrackInfoDialog( color.close(); }, lighterDialogColor: false, - dialog: StreamBuilder( - initialData: color.value, - stream: color.stream, - builder: (context, snapshot) { - final theme = AppThemes.inst.getAppTheme(snapshot.data, null, true); + dialog: ObxO( + rx: color, + builder: (dialogColor) { + final theme = AppThemes.inst.getAppTheme(dialogColor, null, true); return AnimatedTheme( data: theme, child: CustomBlurryDialog( @@ -176,10 +172,11 @@ Future showTrackInfoDialog( normalTitleStyle: true, title: lang.TRACK_INFO, trailingWidgets: [ - Obx( - () => NamidaIconButton( + ObxO( + rx: settings.showUnknownFieldsInTrackInfoDialog, + builder: (showUnknownFieldsInTrackInfoDialog) => NamidaIconButton( tooltip: lang.SHOW_HIDE_UNKNOWN_FIELDS, - icon: settings.showUnknownFieldsInTrackInfoDialog.value ? Broken.eye : Broken.eye_slash, + icon: showUnknownFieldsInTrackInfoDialog ? Broken.eye : Broken.eye_slash, iconColor: theme.colorScheme.primary, onPressed: () => settings.save(showUnknownFieldsInTrackInfoDialog: !settings.showUnknownFieldsInTrackInfoDialog.value), ), @@ -198,254 +195,252 @@ Future showTrackInfoDialog( ], icon: Broken.info_circle, child: SizedBox( - height: Get.height * 0.7, - width: Get.width, - child: Obx( - () { - settings.showUnknownFieldsInTrackInfoDialog.value; - return CustomScrollView( - slivers: [ - SliverList( - delegate: SliverChildListDelegate( - [ - const SizedBox(height: 12.0), - NamidaInkWell( - onTap: () => showTrackListensDialog(track, datesOfListen: totalListens, colorScheme: color.value), - borderRadius: 12.0, - child: Row( - children: [ - const SizedBox(width: 2.0), - TapDetector( - onTap: () => NamidaNavigator.inst.navigateDialog( - scale: 1.0, - blackBg: true, - dialog: LongPressDetector( - onLongPress: () async { - final saveDirPath = await EditDeleteController.inst.saveArtworkToStorage(track); - String title = lang.COPIED_ARTWORK; - String subtitle = '${lang.SAVED_IN} $saveDirPath'; - Color snackColor = color.value; - - if (saveDirPath == null) { - title = lang.ERROR; - subtitle = lang.COULDNT_SAVE_IMAGE; - snackColor = Colors.red; - } - snackyy( - title: title, - message: subtitle, - leftBarIndicatorColor: snackColor, - margin: EdgeInsets.zero, - top: false, - borderRadius: 0, - ); - }, - child: PhotoView( - heroAttributes: PhotoViewHeroAttributes(tag: '$comingFromQueue${index}_sussydialogs_${trackExt.path}$additionalHero'), - gaplessPlayback: true, - tightMode: true, - minScale: PhotoViewComputedScale.contained, - loadingBuilder: (context, event) => artwork, - backgroundDecoration: const BoxDecoration(color: Colors.transparent), - filterQuality: FilterQuality.high, - imageProvider: FileImage(File(trackExt.pathToImage)), - ), + height: namida.height * 0.7, + width: namida.width, + child: ObxO( + rx: settings.showUnknownFieldsInTrackInfoDialog, + builder: (_) => CustomScrollView( + slivers: [ + SliverList( + delegate: SliverChildListDelegate( + [ + const SizedBox(height: 12.0), + NamidaInkWell( + onTap: () => showTrackListensDialog(track, datesOfListen: totalListens, colorScheme: color.value), + borderRadius: 12.0, + child: Row( + children: [ + const SizedBox(width: 2.0), + TapDetector( + onTap: () => NamidaNavigator.inst.navigateDialog( + scale: 1.0, + blackBg: true, + dialog: LongPressDetector( + onLongPress: () async { + final saveDirPath = await EditDeleteController.inst.saveArtworkToStorage(track); + String title = lang.COPIED_ARTWORK; + String subtitle = '${lang.SAVED_IN} $saveDirPath'; + Color snackColor = color.value; + + if (saveDirPath == null) { + title = lang.ERROR; + subtitle = lang.COULDNT_SAVE_IMAGE; + snackColor = Colors.red; + } + snackyy( + title: title, + message: subtitle, + leftBarIndicatorColor: snackColor, + margin: EdgeInsets.zero, + top: false, + borderRadius: 0, + ); + }, + child: PhotoView( + heroAttributes: PhotoViewHeroAttributes(tag: '$comingFromQueue${index}_sussydialogs_${trackExt.path}$additionalHero'), + gaplessPlayback: true, + tightMode: true, + minScale: PhotoViewComputedScale.contained, + loadingBuilder: (context, event) => artwork, + backgroundDecoration: const BoxDecoration(color: Colors.transparent), + filterQuality: FilterQuality.high, + imageProvider: FileImage(File(trackExt.pathToImage)), ), ), - child: artwork, ), - const SizedBox(width: 10.0), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon( - Broken.hashtag_1, - size: 18.0, - ), - const SizedBox(width: 4.0), - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text( - '${lang.TOTAL_LISTENS}: ', - style: theme.textTheme.displaySmall, - ), - Text( - '${totalListens.length}', - style: theme.textTheme.displaySmall?.copyWith(color: theme.colorScheme.primary, fontWeight: FontWeight.w600), - ), - ], - ), - ], - ), - const SizedBox(height: 8.0), - Row( - children: [ - const Icon( - Broken.cake, - size: 18.0, - ), - const SizedBox(width: 4.0), - Expanded( - child: Text( - firstListenTrack?.dateAndClockFormattedOriginal ?? lang.MAKE_YOUR_FIRST_LISTEN, + child: artwork, + ), + const SizedBox(width: 10.0), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon( + Broken.hashtag_1, + size: 18.0, + ), + const SizedBox(width: 4.0), + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + '${lang.TOTAL_LISTENS}: ', style: theme.textTheme.displaySmall, ), + Text( + '${totalListens.length}', + style: theme.textTheme.displaySmall?.copyWith(color: theme.colorScheme.primary, fontWeight: FontWeight.w600), + ), + ], + ), + ], + ), + const SizedBox(height: 8.0), + Row( + children: [ + const Icon( + Broken.cake, + size: 18.0, + ), + const SizedBox(width: 4.0), + Expanded( + child: Text( + firstListenTrack?.dateAndClockFormattedOriginal ?? lang.MAKE_YOUR_FIRST_LISTEN, + style: theme.textTheme.displaySmall, ), - ], - ), - ], - ), + ), + ], + ), + ], ), - const SizedBox(width: 12.0), - ], - ), + ), + const SizedBox(width: 12.0), + ], + ), + ), + const SizedBox(height: 12.0), + if (shouldShowTheField(trackExt.hasUnknownTitle)) + TrackInfoListTile( + title: lang.TITLE, + value: trackExt.title, + icon: Broken.text, ), - const SizedBox(height: 12.0), - if (shouldShowTheField(trackExt.hasUnknownTitle)) - TrackInfoListTile( - title: lang.TITLE, - value: trackExt.title, - icon: Broken.text, - ), - - if (shouldShowTheField(trackExt.hasUnknownArtist)) - TrackInfoListTile( - title: Indexer.splitArtist( - title: trackExt.title, - originalArtist: trackExt.originalArtist, - config: ArtistsSplitConfig.settings(addFeatArtist: false), - ).length == - 1 - ? lang.ARTIST - : lang.ARTISTS, - value: trackExt.hasUnknownArtist ? UnknownTags.ARTIST : trackExt.originalArtist, - icon: Broken.microphone, - ), - - if (shouldShowTheField(trackExt.hasUnknownAlbum)) - TrackInfoListTile( - title: lang.ALBUM, - value: trackExt.hasUnknownAlbum ? UnknownTags.ALBUM : trackExt.album, - icon: Broken.music_dashboard, - ), - - if (shouldShowTheField(trackExt.hasUnknownAlbumArtist)) - TrackInfoListTile( - title: lang.ALBUM_ARTIST, - value: trackExt.hasUnknownAlbumArtist ? UnknownTags.ALBUMARTIST : trackExt.albumArtist, - icon: Broken.user, - ), - - if (shouldShowTheField(trackExt.hasUnknownGenre)) - TrackInfoListTile( - title: trackExt.genresList.length == 1 ? lang.GENRE : lang.GENRES, - value: trackExt.hasUnknownGenre ? UnknownTags.GENRE : trackExt.genresList.join(', '), - icon: trackExt.genresList.length == 1 ? Broken.emoji_happy : Broken.smileys, - ), - - if (shouldShowTheField(trackExt.hasUnknownMood)) - TrackInfoListTile( - title: trackExt.moodList.length == 1 ? lang.MOOD : lang.MOODS, - value: trackExt.hasUnknownMood ? UnknownTags.MOOD : trackExt.moodList.join(', '), - icon: Broken.happyemoji, - ), - - if (shouldShowTheField(trackExt.hasUnknownComposer)) - TrackInfoListTile( - title: lang.COMPOSER, - value: trackExt.hasUnknownComposer ? UnknownTags.COMPOSER : trackExt.composer, - icon: Broken.profile_2user, - ), - - if (shouldShowTheField(trackExt.duration == 0)) - TrackInfoListTile( - title: lang.DURATION, - value: trackExt.duration.secondsLabel, - icon: Broken.clock, - ), - - if (shouldShowTheField(trackExt.year == 0)) - TrackInfoListTile( - title: lang.YEAR, - value: trackExt.year == 0 ? '?' : '${trackExt.year} (${trackExt.year.yearFormatted}${releasedFromNow == '' ? '' : ' | $releasedFromNow'})', - icon: Broken.calendar, - ), - - if (shouldShowTheField(trackExt.dateModified == 0)) - TrackInfoListTile( - title: lang.DATE_MODIFIED, - value: trackExt.dateModified.dateAndClockFormattedOriginal, - icon: Broken.calendar_1, - ), - - /// - if (shouldShowTheField(trackExt.discNo == 0)) - TrackInfoListTile( - title: lang.DISC_NUMBER, - value: trackExt.discNo.toString(), - icon: Broken.hashtag, - ), - - if (shouldShowTheField(trackExt.trackNo == 0)) - TrackInfoListTile( - title: lang.TRACK_NUMBER, - value: trackExt.trackNo.toString(), - icon: Broken.hashtag, - ), - - /// bruh moment - if (shouldShowTheField(trackExt.filenameWOExt == '')) - TrackInfoListTile( - title: lang.FILE_NAME, - value: trackExt.filenameWOExt, - icon: Broken.quote_up_circle, - ), - - if (shouldShowTheField(trackExt.folderName == '')) - TrackInfoListTile( - title: lang.FOLDER, - value: trackExt.folderName, - icon: Broken.folder, - ), - - if (shouldShowTheField(trackExt.path == '')) - TrackInfoListTile( - title: lang.PATH, - value: trackExt.path, - icon: Broken.location, - ), + if (shouldShowTheField(trackExt.hasUnknownArtist)) TrackInfoListTile( - title: lang.FORMAT, - value: '${track.audioInfoFormattedCompact}\n${trackExt.extension} - ${trackExt.size.fileSizeFormatted}', - icon: Broken.voice_cricle, + title: Indexer.splitArtist( + title: trackExt.title, + originalArtist: trackExt.originalArtist, + config: ArtistsSplitConfig.settings(addFeatArtist: false), + ).length == + 1 + ? lang.ARTIST + : lang.ARTISTS, + value: trackExt.hasUnknownArtist ? UnknownTags.ARTIST : trackExt.originalArtist, + icon: Broken.microphone, ), - if (shouldShowTheField(trackExt.lyrics == '')) - TrackInfoListTile( - title: lang.LYRICS, - value: trackExt.lyrics, - icon: trackExt.lyrics.isEmpty ? Broken.note_remove : Broken.message_text, - ), - - if (shouldShowTheField(trackExt.comment == '')) - TrackInfoListTile( - title: lang.COMMENT, - value: trackExt.comment, - icon: Broken.message_text_1, - isComment: true, - ), - const SizedBox(height: 12.0), - ].addSeparators(separator: NamidaContainerDivider(color: color.value), skipFirst: 3).toList(), - ), + if (shouldShowTheField(trackExt.hasUnknownAlbum)) + TrackInfoListTile( + title: lang.ALBUM, + value: trackExt.hasUnknownAlbum ? UnknownTags.ALBUM : trackExt.album, + icon: Broken.music_dashboard, + ), + + if (shouldShowTheField(trackExt.hasUnknownAlbumArtist)) + TrackInfoListTile( + title: lang.ALBUM_ARTIST, + value: trackExt.hasUnknownAlbumArtist ? UnknownTags.ALBUMARTIST : trackExt.albumArtist, + icon: Broken.user, + ), + + if (shouldShowTheField(trackExt.hasUnknownGenre)) + TrackInfoListTile( + title: trackExt.genresList.length == 1 ? lang.GENRE : lang.GENRES, + value: trackExt.hasUnknownGenre ? UnknownTags.GENRE : trackExt.genresList.join(', '), + icon: trackExt.genresList.length == 1 ? Broken.emoji_happy : Broken.smileys, + ), + + if (shouldShowTheField(trackExt.hasUnknownMood)) + TrackInfoListTile( + title: trackExt.moodList.length == 1 ? lang.MOOD : lang.MOODS, + value: trackExt.hasUnknownMood ? UnknownTags.MOOD : trackExt.moodList.join(', '), + icon: Broken.happyemoji, + ), + + if (shouldShowTheField(trackExt.hasUnknownComposer)) + TrackInfoListTile( + title: lang.COMPOSER, + value: trackExt.hasUnknownComposer ? UnknownTags.COMPOSER : trackExt.composer, + icon: Broken.profile_2user, + ), + + if (shouldShowTheField(trackExt.duration == 0)) + TrackInfoListTile( + title: lang.DURATION, + value: trackExt.duration.secondsLabel, + icon: Broken.clock, + ), + + if (shouldShowTheField(trackExt.year == 0)) + TrackInfoListTile( + title: lang.YEAR, + value: trackExt.year == 0 ? '?' : '${trackExt.year} (${trackExt.year.yearFormatted}${releasedFromNow == '' ? '' : ' | $releasedFromNow'})', + icon: Broken.calendar, + ), + + if (shouldShowTheField(trackExt.dateModified == 0)) + TrackInfoListTile( + title: lang.DATE_MODIFIED, + value: trackExt.dateModified.dateAndClockFormattedOriginal, + icon: Broken.calendar_1, + ), + + /// + if (shouldShowTheField(trackExt.discNo == 0)) + TrackInfoListTile( + title: lang.DISC_NUMBER, + value: trackExt.discNo.toString(), + icon: Broken.hashtag, + ), + + if (shouldShowTheField(trackExt.trackNo == 0)) + TrackInfoListTile( + title: lang.TRACK_NUMBER, + value: trackExt.trackNo.toString(), + icon: Broken.hashtag, + ), + + /// bruh moment + if (shouldShowTheField(trackExt.filenameWOExt == '')) + TrackInfoListTile( + title: lang.FILE_NAME, + value: trackExt.filenameWOExt, + icon: Broken.quote_up_circle, + ), + + if (shouldShowTheField(trackExt.folderName == '')) + TrackInfoListTile( + title: lang.FOLDER, + value: trackExt.folderName, + icon: Broken.folder, + ), + + if (shouldShowTheField(trackExt.path == '')) + TrackInfoListTile( + title: lang.PATH, + value: trackExt.path, + icon: Broken.location, + ), + + TrackInfoListTile( + title: lang.FORMAT, + value: '${track.audioInfoFormattedCompact}\n${trackExt.extension} - ${trackExt.size.fileSizeFormatted}', + icon: Broken.voice_cricle, + ), + + if (shouldShowTheField(trackExt.lyrics == '')) + TrackInfoListTile( + title: lang.LYRICS, + value: trackExt.lyrics, + icon: trackExt.lyrics.isEmpty ? Broken.note_remove : Broken.message_text, + ), + + if (shouldShowTheField(trackExt.comment == '')) + TrackInfoListTile( + title: lang.COMMENT, + value: trackExt.comment, + icon: Broken.message_text_1, + isComment: true, + ), + const SizedBox(height: 12.0), + ].addSeparators(separator: NamidaContainerDivider(color: color.value), skipFirst: 3).toList(), ), - ], - ); - }, + ), + ], + ), ), ), ), @@ -500,12 +495,12 @@ class TrackInfoListTile extends StatelessWidget { Icon( icon, size: 17.0, - color: context.theme.colorScheme.onBackground.withAlpha(220), + color: context.theme.colorScheme.onSurface.withAlpha(220), ), const SizedBox(width: 6.0), Text( '$title:', - style: context.theme.textTheme.displaySmall?.copyWith(color: context.theme.colorScheme.onBackground.withAlpha(220)), + style: context.theme.textTheme.displaySmall?.copyWith(color: context.theme.colorScheme.onSurface.withAlpha(220)), ), const SizedBox(width: 4.0), child ?? @@ -515,7 +510,7 @@ class TrackInfoListTile extends StatelessWidget { value == '' ? '?' : value, style: context.theme.textTheme.displayMedium?.copyWith( color: Color.alphaBlend(context.theme.colorScheme.primary.withAlpha(140), context.textTheme.displayMedium!.color!), - fontSize: 13.5.multipliedFontScale, + fontSize: 13.5, ), )), ], diff --git a/lib/ui/dialogs/track_listens_dialog.dart b/lib/ui/dialogs/track_listens_dialog.dart index c1af4e43..557ed135 100644 --- a/lib/ui/dialogs/track_listens_dialog.dart +++ b/lib/ui/dialogs/track_listens_dialog.dart @@ -1,8 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; - -import 'package:get/get.dart'; import 'package:jiffy/jiffy.dart'; import 'package:paged_vertical_calendar/paged_vertical_calendar.dart'; @@ -17,6 +15,7 @@ import 'package:namida/core/functions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/themes.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; void showTrackListensDialog(Track track, {List datesOfListen = const [], Color? colorScheme}) async { @@ -47,7 +46,7 @@ void showListensDialog({ if (datesOfListen.isEmpty) return; datesOfListen.sortByReverse((e) => e); - final color = (colorScheme ?? CurrentColor.inst.color).obs; + final color = (colorScheme ?? CurrentColor.inst.color).obso; if (colorScheme == null && colorSchemeFunction != null) { colorSchemeFunction().executeWithMinDelay(delayMS: 100).then((c) { @@ -73,11 +72,10 @@ void showListensDialog({ color.close(); }, lighterDialogColor: false, - dialog: StreamBuilder( - initialData: color.value, - stream: color.stream, - builder: (context, snapshot) { - final theme = AppThemes.inst.getAppTheme(snapshot.data, null, false); + dialog: ObxO( + rx: color, + builder: (dialogColor) { + final theme = AppThemes.inst.getAppTheme(dialogColor, null, false); return AnimatedTheme( data: theme, child: CustomBlurryDialog( @@ -90,19 +88,21 @@ void showListensDialog({ style: theme.textTheme.displaySmall?.copyWith(color: theme.colorScheme.primary, fontWeight: FontWeight.w600), ), const SizedBox(width: 8.0), - Obx( - () => NamidaIconButton( - icon: settings.heatmapListensView.value ? Broken.row_vertical : Broken.calendar_1, - iconSize: settings.heatmapListensView.value ? 18.0 : 20.0, + ObxO( + rx: settings.heatmapListensView, + builder: (heatmapListensView) => NamidaIconButton( + icon: heatmapListensView ? Broken.row_vertical : Broken.calendar_1, + iconSize: heatmapListensView ? 18.0 : 20.0, onPressed: () => settings.save(heatmapListensView: !settings.heatmapListensView.value), ), ), ], child: SizedBox( - height: Get.height * 0.5, - width: Get.width, - child: Obx( - () => settings.heatmapListensView.value + height: namida.height * 0.5, + width: namida.width, + child: ObxO( + rx: settings.heatmapListensView, + builder: (heatmapListensView) => heatmapListensView ? Padding( padding: const EdgeInsets.all(12.0), child: PagedVerticalCalendar( @@ -132,7 +132,7 @@ void showListensDialog({ Padding( padding: const EdgeInsets.all(2.0), child: CircleAvatar( - backgroundColor: color.value, + backgroundColor: dialogColor, maxRadius: 5.0, minRadius: 2.0, ), @@ -156,9 +156,9 @@ void showListensDialog({ final isToday = date.toDaysSince1970() == DateTime.now().toDaysSince1970(); final listens = datesMapByDay[date]?.length ?? 0; return NamidaInkWell( - decoration: BoxDecoration(border: isToday ? Border.all(color: color.value) : null), + decoration: BoxDecoration(border: isToday ? Border.all(color: dialogColor) : null), margin: const EdgeInsets.all(2.0), - bgColor: color.value.withAlpha((listens * 5).clamp(0, 255)), // *5 since 50 listens a days is already a lot + bgColor: dialogColor.withAlpha((listens * 5).clamp(0, 255)), // *5 since 50 listens a days is already a lot borderRadius: 6.0, onTap: datesMapByDay[date] == null ? null : () => onListenTap(date.millisecondsSinceEpoch), child: Column( @@ -167,7 +167,7 @@ void showListensDialog({ Text("${date.day}", style: theme.textTheme.displaySmall), if (listens > 0) ...[ const SizedBox(height: 2.0), - Text("$listens", style: theme.textTheme.displaySmall?.copyWith(fontSize: 9.0.multipliedFontScale)), + Text("$listens", style: theme.textTheme.displaySmall?.copyWith(fontSize: 9.0)), ] ], ), @@ -194,7 +194,7 @@ void showListensDialog({ ); }, itemCount: datesOfListen.length, - itemExtents: null, + itemExtent: null, ), ), ), diff --git a/lib/ui/pages/about_page.dart b/lib/ui/pages/about_page.dart index bb3b9be8..d8808f4b 100644 --- a/lib/ui/pages/about_page.dart +++ b/lib/ui/pages/about_page.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_mailer/flutter_mailer.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:markdown/src/ast.dart' as md; import 'package:share_plus/share_plus.dart'; import 'package:http/http.dart' as http; @@ -32,6 +32,8 @@ class AboutPage extends StatefulWidget { } class _AboutPageState extends State { + late final _loadingChangelog = false.obso; + @override void initState() { super.initState(); @@ -42,6 +44,12 @@ class _AboutPageState extends State { } } + @override + void dispose() { + _loadingChangelog.close(); + super.dispose(); + } + String _getDateDifferenceText() { final buildDate = NamidaDeviceInfo.buildDate; if (buildDate == null) return ''; @@ -65,7 +73,6 @@ class _AboutPageState extends State { if (latestRelease == null) return null; if (latestRelease.startsWith('v')) latestRelease = latestRelease.substring(1); if (current.startsWith('v')) current = current.substring(1); - if (latestRelease == current) return null; return latestRelease; } catch (_) {} return null; @@ -247,22 +254,23 @@ class _AboutPageState extends State { subtitle: 'Have an issue or suggestion? open an issue on GitHub', link: AppSocial.GITHUB_ISSUES, ), - ObxValue( - (isLoading) => NamidaAboutListTile( + ObxO( + rx: _loadingChangelog, + builder: (isLoading) => NamidaAboutListTile( icon: Broken.activity, title: lang.CHANGELOG, subtitle: 'See what\'s newly added/fixed inside Namida', - trailing: isLoading.value ? const LoadingIndicator() : null, + trailing: isLoading ? const LoadingIndicator() : null, onTap: () async { - isLoading.value = true; + _loadingChangelog.value = true; final stringy = await http.get(Uri.parse('https://raw.githubusercontent.com/namidaco/namida/main/CHANGELOG.md')); - isLoading.value = false; + _loadingChangelog.value = false; await Future.delayed(Duration.zero); // delay bcz sometimes doesnt show - // ignore: use_build_context_synchronously showModalBottomSheet( showDragHandle: true, useRootNavigator: true, isScrollControlled: true, + // ignore: use_build_context_synchronously context: context, builder: (context) { return SizedBox( @@ -290,7 +298,6 @@ class _AboutPageState extends State { ); }, ), - false.obs, ), NamidaAboutListTile( icon: Broken.language_circle, @@ -435,18 +442,18 @@ class _NamidaMarkdownElementBuilderHeader extends MarkdownElementBuilder { NamidaLinkUtils.openLink(url); } }, - bgColor: Get.theme.cardTheme.color?.withOpacity(0.8), + bgColor: namida.theme.cardTheme.color?.withOpacity(0.8), borderRadius: 18.0, decoration: BoxDecoration( border: Border.all( width: 1.5, - color: Get.theme.colorScheme.primary.withOpacity(0.5), + color: namida.theme.colorScheme.primary.withOpacity(0.5), ), ), padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), child: Text( text.text, - style: Get.textTheme.displayMedium, + style: namida.textTheme.displayMedium, ), ), ); @@ -467,20 +474,20 @@ class _NamidaMarkdownElementBuilderCommitLink extends MarkdownElementBuilder { final url = "${AppSocial.GITHUB}/commit/$longHash"; final textWithoutCommit = longHash == null ? text.text : text.text.replaceFirst(regex, ''); final commit = shortenLongHash(longHash); - return RichText( - text: TextSpan( + return Text.rich( + TextSpan( text: commit == null ? '' : "#$commit:", - style: Get.textTheme.displayMedium?.copyWith( - fontSize: 13.5.multipliedFontScale, - color: Get.theme.colorScheme.secondary, + style: namida.textTheme.displayMedium?.copyWith( + fontSize: 13.5, + color: namida.theme.colorScheme.secondary, ), recognizer: TapGestureRecognizer()..onTap = () => NamidaLinkUtils.openLink(url), children: [ TextSpan( text: textWithoutCommit, - style: Get.textTheme.displaySmall?.copyWith( + style: namida.textTheme.displaySmall?.copyWith( fontWeight: FontWeight.w400, - fontSize: 13.0.multipliedFontScale, + fontSize: 13.0, ), ), ], diff --git a/lib/ui/pages/albums_page.dart b/lib/ui/pages/albums_page.dart index fc1e7aef..6d877baf 100644 --- a/lib/ui/pages/albums_page.dart +++ b/lib/ui/pages/albums_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/controller/scroll_search_controller.dart'; import 'package:namida/controller/search_sort_controller.dart'; @@ -36,7 +36,7 @@ class AlbumsPage extends StatelessWidget { @override Widget build(BuildContext context) { - final finalAlbums = albumIdentifiers ?? SearchSortController.inst.albumSearchList; + final finalAlbums = albumIdentifiers ?? SearchSortController.inst.albumSearchList.value; final scrollController = LibraryTab.albums.scrollController; final albumDimensions = Dimensions.inst.getAlbumCardDimensions(countPerRow); @@ -51,24 +51,24 @@ class AlbumsPage extends StatelessWidget { enableHero: enableHero, gridWidget: ChangeGridCountWidget( currentCount: countPerRow, - forStaggered: settings.useAlbumStaggeredGridView.value, + forStaggered: settings.useAlbumStaggeredGridView.valueR, onTap: () { final newCount = ScrollSearchController.inst.animateChangingGridSize(LibraryTab.albums, countPerRow, animateTiles: false); settings.save(albumGridCount: newCount); }, ), - isBarVisible: LibraryTab.albums.isBarVisible, - showSearchBox: LibraryTab.albums.isSearchBoxVisible, + isBarVisible: LibraryTab.albums.isBarVisible.valueR, + showSearchBox: LibraryTab.albums.isSearchBoxVisible.valueR, leftText: finalAlbums.length.displayAlbumKeyword, onFilterIconTap: () => ScrollSearchController.inst.switchSearchBoxVisibilty(LibraryTab.albums), onCloseButtonPressed: () => ScrollSearchController.inst.clearSearchTextField(LibraryTab.albums), sortByMenuWidget: SortByMenu( - title: settings.albumSort.value.toText(), + title: settings.albumSort.valueR.toText(), popupMenuChild: () => const SortByMenuAlbums(), - isCurrentlyReversed: settings.albumSortReversed.value, + isCurrentlyReversed: settings.albumSortReversed.valueR, onReverseIconTap: () => SearchSortController.inst.sortMedia(MediaType.album, reverse: !settings.albumSortReversed.value), ), - textField: CustomTextFiled( + textField: () => CustomTextFiled( textFieldController: LibraryTab.albums.textSearchController, textFieldHintText: lang.FILTER_ALBUMS, onTextFieldValueChanged: (value) => SearchSortController.inst.searchMedia(value, MediaType.album), @@ -77,13 +77,13 @@ class AlbumsPage extends StatelessWidget { ), Obx( () { - settings.albumListTileHeight.value; + settings.albumListTileHeight.valueR; return countPerRow == 1 ? Expanded( child: ListView.builder( controller: scrollController, itemCount: finalAlbums.length, - itemExtent: settings.albumListTileHeight.value + 4.0 * 5, + itemExtent: settings.albumListTileHeight.valueR + 4.0 * 5, padding: kBottomPaddingInsets, itemBuilder: (BuildContext context, int i) { final albumId = finalAlbums[i]; @@ -98,7 +98,7 @@ class AlbumsPage extends StatelessWidget { }, ), ) - : settings.useAlbumStaggeredGridView.value + : settings.useAlbumStaggeredGridView.valueR ? Expanded( child: MasonryGridView.builder( controller: scrollController, diff --git a/lib/ui/pages/artists_page.dart b/lib/ui/pages/artists_page.dart index 5e451aa4..4df33470 100644 --- a/lib/ui/pages/artists_page.dart +++ b/lib/ui/pages/artists_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/controller/scroll_search_controller.dart'; import 'package:namida/controller/search_sort_controller.dart'; @@ -79,7 +79,7 @@ class ArtistsPage extends StatelessWidget { @override Widget build(BuildContext context) { - final finalArtists = artists ?? SearchSortController.inst.artistSearchList; + final finalArtists = artists ?? SearchSortController.inst.artistSearchList.value; final scrollController = LibraryTab.artists.scrollController; final artistDimensions = Dimensions.inst.getArtistCardDimensions(countPerRow); @@ -90,7 +90,7 @@ class ArtistsPage extends StatelessWidget { child: AnimationLimiter( child: Obx( () { - final artistTypeSettings = settings.activeArtistType.value; + final artistTypeSettings = settings.activeArtistType.valueR; final artistType = customType ?? artistTypeSettings; final artistTypeText = artistType.toText(); final artistLeftText = finalArtists.length.displayKeyword(artistTypeText, artistTypeText); @@ -99,14 +99,14 @@ class ArtistsPage extends StatelessWidget { ExpandableBox( enableHero: enableHero, gridWidget: ChangeGridCountWidget( - currentCount: settings.artistGridCount.value, + currentCount: settings.artistGridCount.valueR, onTap: () { final newCount = ScrollSearchController.inst.animateChangingGridSize(LibraryTab.artists, countPerRow); settings.save(artistGridCount: newCount); }, ), - isBarVisible: LibraryTab.artists.isBarVisible, - showSearchBox: LibraryTab.artists.isSearchBoxVisible, + isBarVisible: LibraryTab.artists.isBarVisible.valueR, + showSearchBox: LibraryTab.artists.isSearchBoxVisible.valueR, leftText: customType != null ? artistLeftText : '', leftWidgets: customType != null ? [] @@ -135,12 +135,12 @@ class ArtistsPage extends StatelessWidget { onFilterIconTap: () => ScrollSearchController.inst.switchSearchBoxVisibilty(LibraryTab.artists), onCloseButtonPressed: () => ScrollSearchController.inst.clearSearchTextField(LibraryTab.artists), sortByMenuWidget: SortByMenu( - title: settings.artistSort.value.toText(), + title: settings.artistSort.valueR.toText(), popupMenuChild: () => const SortByMenuArtists(), - isCurrentlyReversed: settings.artistSortReversed.value, + isCurrentlyReversed: settings.artistSortReversed.valueR, onReverseIconTap: () => SearchSortController.inst.sortMedia(settings.activeArtistType.value, reverse: !settings.artistSortReversed.value), ), - textField: CustomTextFiled( + textField: () => CustomTextFiled( textFieldController: LibraryTab.artists.textSearchController, textFieldHintText: lang.FILTER_ARTISTS, onTextFieldValueChanged: (value) => SearchSortController.inst.searchMedia(value, settings.activeArtistType.value), diff --git a/lib/ui/pages/equalizer_page.dart b/lib/ui/pages/equalizer_page.dart index be4efcc5..5f88099e 100644 --- a/lib/ui/pages/equalizer_page.dart +++ b/lib/ui/pages/equalizer_page.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:just_audio/just_audio.dart'; import 'package:vibration/vibration.dart'; @@ -15,6 +14,7 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/animated_widgets.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; @@ -41,7 +41,7 @@ class EqualizerMainSlidersColumn extends StatelessWidget { () => _SliderTextWidget( icon: Broken.airpods, title: lang.PITCH, - value: settings.player.pitch.value, + value: settings.player.pitch.valueR, restoreDefault: () { Player.inst.setPlayerPitch(1.0); settings.player.save(pitch: 1.0); @@ -63,7 +63,7 @@ class EqualizerMainSlidersColumn extends StatelessWidget { () => _SliderTextWidget( icon: Broken.forward, title: lang.SPEED, - value: settings.player.speed.value, + value: settings.player.speed.valueR, restoreDefault: () { Player.inst.setPlayerSpeed(1.0); settings.player.save(speed: 1.0); @@ -84,9 +84,9 @@ class EqualizerMainSlidersColumn extends StatelessWidget { verticalPadding, Obx( () => _SliderTextWidget( - icon: settings.player.volume.value > 0 ? Broken.volume_up : Broken.volume_slash, + icon: settings.player.volume.valueR > 0 ? Broken.volume_up : Broken.volume_slash, title: lang.VOLUME, - value: settings.player.volume.value, + value: settings.player.volume.valueR, restoreDefault: () { Player.inst.setPlayerVolume(1.0); settings.player.save(volume: 1.0); @@ -110,7 +110,7 @@ class EqualizerMainSlidersColumn extends StatelessWidget { } class EqualizerPage extends StatefulWidget { - const EqualizerPage({Key? key}) : super(key: key); + const EqualizerPage({super.key}); @override EqualizerPageState createState() => EqualizerPageState(); @@ -205,10 +205,11 @@ class EqualizerPageState extends State { icon: null, iconSize: 24.0, onPressed: () => settings.equalizer.save(uiTapToUpdate: !settings.equalizer.uiTapToUpdate.value), - child: Obx( - () => StackedIcon( + child: ObxO( + rx: settings.equalizer.uiTapToUpdate, + builder: (val) => StackedIcon( baseIcon: Broken.mouse_1, - secondaryIcon: settings.equalizer.uiTapToUpdate.value ? Broken.tick_circle : Broken.close_circle, + secondaryIcon: val ? Broken.tick_circle : Broken.close_circle, secondaryIconSize: 12.0, iconSize: 24.0, ), @@ -275,7 +276,7 @@ class EqualizerPageState extends State { borderRadius: 5.0, margin: const EdgeInsets.symmetric(horizontal: 4.0), padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - bgColor: _activePresetCustom.value + bgColor: _activePresetCustom.valueR ? Color.alphaBlend(CurrentColor.inst.color.withOpacity(0.9), context.theme.scaffoldBackgroundColor) : context.theme.colorScheme.secondary.withOpacity(0.15), onTap: _resetPreset, @@ -283,8 +284,8 @@ class EqualizerPageState extends State { lang.CUSTOM, style: context.textTheme.displaySmall?.copyWith( fontWeight: FontWeight.w700, - fontSize: 13.5.multipliedFontScale, - color: _activePresetCustom.value ? Colors.white.withOpacity(0.7) : null, + fontSize: 13.5, + color: _activePresetCustom.valueR ? Colors.white.withOpacity(0.7) : null, ), ), ), @@ -296,7 +297,7 @@ class EqualizerPageState extends State { borderRadius: 5.0, margin: const EdgeInsets.symmetric(horizontal: 4.0), padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - bgColor: _activePreset.value == e.value + bgColor: _activePreset.valueR == e.value ? Color.alphaBlend(CurrentColor.inst.color.withOpacity(0.9), context.theme.scaffoldBackgroundColor) : context.theme.colorScheme.secondary.withOpacity(0.15), onTap: () async { @@ -309,9 +310,9 @@ class EqualizerPageState extends State { child: Text( e.value, style: context.textTheme.displaySmall?.copyWith( - color: _activePreset.value == e.value ? Colors.white.withOpacity(0.7) : null, + color: _activePreset.valueR == e.value ? Colors.white.withOpacity(0.7) : null, fontWeight: FontWeight.w700, - fontSize: 13.5.multipliedFontScale, + fontSize: 13.5, ), ), ), @@ -357,8 +358,9 @@ class EqualizerPageState extends State { ), ), ), - Obx( - () => _CuteSlider( + ObxO( + rx: settings.equalizer.uiTapToUpdate, + builder: (uiTapToUpdate) => _CuteSlider( key: _loudnessKey, initialValue: targetGain, min: -1, @@ -367,7 +369,7 @@ class EqualizerPageState extends State { settings.equalizer.save(loudnessEnhancer: newVal); _loudnessEnhancer.setTargetGain(newVal); }, - tapToUpdate: settings.equalizer.uiTapToUpdate.value, + tapToUpdate: uiTapToUpdate, ), ), ], @@ -376,10 +378,11 @@ class EqualizerPageState extends State { ); }, ), - Obx( - () => EqualizerMainSlidersColumn( + ObxO( + rx: settings.equalizer.uiTapToUpdate, + builder: (uiTapToUpdate) => EqualizerMainSlidersColumn( verticalInBetweenPadding: verticalInBetweenPaddingH, - tapToUpdate: settings.equalizer.uiTapToUpdate.value, + tapToUpdate: uiTapToUpdate, ), ), verticalInBetweenPadding, @@ -476,7 +479,7 @@ class _CuteSliderState extends State<_CuteSlider> { late double _currentVal = widget.initialValue; void _updateVal(double newVal) { - final finalVal = newVal.toPrecision(4); + final finalVal = newVal.roundDecimals(4); if (finalVal != _currentVal) { setState(() { _currentVal = finalVal; @@ -552,21 +555,21 @@ class EqualizerControls extends StatelessWidget { final bool Function() tapToUpdate; const EqualizerControls({ - Key? key, + super.key, required this.equalizer, required this.onGainSetCallback, required this.tapToUpdate, - }) : super(key: key); + }); void _onGainSet(AndroidEqualizerBand band, AndroidEqualizerParameters parameters, double newValue) { - final newVal = newValue.clamp(parameters.minDecibels, parameters.maxDecibels).toPrecision(4); + final newVal = newValue.clamp(parameters.minDecibels, parameters.maxDecibels).roundDecimals(4); settings.equalizer.save(equalizerValue: MapEntry(band.centerFrequency, newVal)); band.setGain(newVal); onGainSetCallback(); } void _onGainSetNoClamp(AndroidEqualizerBand band, AndroidEqualizerParameters parameters, double newValue) { - final newVal = newValue.toPrecision(4); + final newVal = newValue.roundDecimals(4); settings.equalizer.save(equalizerValue: MapEntry(band.centerFrequency, newVal)); band.setGain(newValue); onGainSetCallback(); @@ -670,14 +673,14 @@ class VerticalSlider extends StatefulWidget { final bool Function() tapToUpdate; const VerticalSlider({ - Key? key, + super.key, required this.value, this.min = 0.0, this.max = 1.0, required this.circleWidth, required this.onChanged, required this.tapToUpdate, - }) : super(key: key); + }); @override State createState() => _VerticalSliderState(); @@ -771,7 +774,7 @@ class _VerticalSliderState extends State { child: Obx( () => AnimatedScale( duration: const Duration(milliseconds: 200), - scale: _isPointerDown.value ? 1.2 : 1.0, + scale: _isPointerDown.valueR ? 1.2 : 1.0, child: Container( width: circleWidth, height: circleHeight, diff --git a/lib/ui/pages/folders_page.dart b/lib/ui/pages/folders_page.dart index 985f4a23..eb1953b7 100644 --- a/lib/ui/pages/folders_page.dart +++ b/lib/ui/pages/folders_page.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - import 'package:namida/class/folder.dart'; import 'package:namida/controller/folders_controller.dart'; import 'package:namida/controller/indexer_controller.dart'; @@ -14,6 +12,7 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/functions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/library/folder_tile.dart'; import 'package:namida/ui/widgets/library/track_tile.dart'; @@ -21,11 +20,12 @@ import 'package:namida/ui/widgets/library/track_tile.dart'; class FoldersPage extends StatelessWidget { const FoldersPage({super.key}); - Widget get iconWidget => Obx( - () => SizedBox( + Widget get iconWidget => ObxO( + rx: Folders.inst.isHome, + builder: (isHome) => SizedBox( height: double.infinity, child: Icon( - Folders.inst.isHome.value ? Broken.home_2 : Broken.folder_2, + isHome ? Broken.home_2 : Broken.folder_2, size: 22.0, ), ), @@ -34,14 +34,16 @@ class FoldersPage extends StatelessWidget { @override Widget build(BuildContext context) { final ScrollController scrollController = LibraryTab.folders.scrollController; - final highlighedColor = context.theme.colorScheme.onBackground.withAlpha(40); + final highlighedColor = context.theme.colorScheme.onSurface.withAlpha(40); + const scrollToIconSize = 24.0; + const scrollToiconBottomPaddingSliver = SliverPadding(padding: EdgeInsets.only(bottom: scrollToIconSize * 2)); return BackgroundWrapper( child: Stack( children: [ Obx( () { final mainMapFoldersKeys = Indexer.inst.mainMapFolders.keys.toList(); - return settings.enableFoldersHierarchy.value + return settings.enableFoldersHierarchy.valueR // == Folders in heirarchy ? Column( @@ -51,8 +53,8 @@ class FoldersPage extends StatelessWidget { child: Obx( () => CustomListTile( borderR: 16.0, - icon: Folders.inst.isHome.value ? Broken.home_2 : Broken.folder_2, - title: Folders.inst.currentFolder.value?.path.formatPath() ?? lang.HOME, + icon: Folders.inst.isHome.valueR ? Broken.home_2 : Broken.folder_2, + title: Folders.inst.currentFolder.valueR?.path.formatPath() ?? lang.HOME, titleStyle: context.textTheme.displaySmall, onTap: () => Folders.inst.stepOut(), trailingRaw: Row( @@ -60,11 +62,11 @@ class FoldersPage extends StatelessWidget { children: [ Obx( () { - final pathOfDefault = Folders.inst.isHome.value ? '' : Folders.inst.currentFolder.value?.path; + final pathOfDefault = Folders.inst.isHome.valueR ? '' : Folders.inst.currentFolder.valueR?.path; return NamidaIconButton( horizontalPadding: 8.0, tooltip: lang.SET_AS_DEFAULT, - icon: settings.defaultFolderStartupLocation.value == pathOfDefault ? Broken.archive_tick : Broken.save_2, + icon: settings.defaultFolderStartupLocation.valueR == pathOfDefault ? Broken.archive_tick : Broken.save_2, iconSize: 22.0, onPressed: () => settings.save( defaultFolderStartupLocation: Folders.inst.currentFolder.value?.path ?? '', @@ -88,45 +90,49 @@ class FoldersPage extends StatelessWidget { child: NamidaScrollbar( controller: scrollController, child: Obx( - () => CustomScrollView( - controller: scrollController, - slivers: [ - if (Folders.inst.isHome.value) - SliverList.builder( - itemCount: kStoragePaths.length, - itemBuilder: (context, i) { - final p = kStoragePaths.elementAt(i); - return FolderTile( - folder: Folder(p), - dummyTracks: Folder(p).tracksRecusive.toList(), - ); - }, - ), - if (!Folders.inst.isHome.value) ...[ - SliverList.builder( - itemCount: Folders.inst.currentFolderslist.length, - itemBuilder: (context, i) { - return FolderTile( - folder: Folders.inst.currentFolderslist[i], - ); - }, - ), - SliverFixedExtentList.builder( - itemCount: Folders.inst.currentTracks.length, - itemExtent: Dimensions.inst.trackTileItemExtent, - itemBuilder: (context, i) { - return TrackTile( - index: i, - trackOrTwd: Folders.inst.currentTracks[i], - queueSource: QueueSource.folder, - bgColor: i == Folders.inst.indexToScrollTo.value ? highlighedColor : null, - ); - }, - ), + () { + final folderTracks = Folders.inst.currentFolder.valueR?.tracks() ?? []; + return CustomScrollView( + controller: scrollController, + slivers: [ + if (Folders.inst.isHome.valueR) + SliverList.builder( + itemCount: kStoragePaths.length, + itemBuilder: (context, i) { + final p = kStoragePaths.elementAt(i); + return FolderTile( + folder: Folder(p), + dummyTracks: Folder(p).tracksRecusive().toList(), + ); + }, + ), + if (!Folders.inst.isHome.valueR) ...[ + SliverList.builder( + itemCount: Folders.inst.currentFolderslist.length, + itemBuilder: (context, i) { + return FolderTile( + folder: Folders.inst.currentFolderslist[i], + ); + }, + ), + SliverFixedExtentList.builder( + itemCount: folderTracks.length, + itemExtent: Dimensions.inst.trackTileItemExtent, + itemBuilder: (context, i) { + return TrackTile( + index: i, + trackOrTwd: folderTracks[i], + queueSource: QueueSource.folder, + bgColor: i == Folders.inst.indexToScrollTo.value ? highlighedColor : null, + ); + }, + ), + ], + kBottomPaddingWidgetSliver, + scrollToiconBottomPaddingSliver, ], - kBottomPaddingWidgetSliver, - ], - ), + ); + }, ), ), ), @@ -138,9 +144,10 @@ class FoldersPage extends StatelessWidget { children: [ ListTile( leading: iconWidget, - title: Obx( - () => Text( - Folders.inst.currentFolder.value?.path.formatPath() ?? lang.HOME, + title: ObxO( + rx: Folders.inst.currentFolder, + builder: (currentFolder) => Text( + currentFolder?.path.formatPath() ?? lang.HOME, style: context.textTheme.displaySmall, maxLines: 2, overflow: TextOverflow.ellipsis, @@ -152,37 +159,41 @@ class FoldersPage extends StatelessWidget { child: NamidaScrollbar( controller: scrollController, child: Obx( - () => CustomScrollView( - controller: scrollController, - slivers: [ - if (!Folders.inst.isInside.value) - SliverList.builder( - itemCount: Indexer.inst.mainMapFolders.length, + () { + final folderTracks = Folders.inst.currentFolder.valueR?.tracks() ?? []; + return CustomScrollView( + controller: scrollController, + slivers: [ + if (!Folders.inst.isInside.valueR) + SliverList.builder( + itemCount: Indexer.inst.mainMapFolders.length, + itemBuilder: (context, i) { + final folder = mainMapFoldersKeys[i]; + if (folder.tracks().isEmpty) return const SizedBox(); + return FolderTile( + folder: folder, + subtitle: folder.hasSimilarFolderNames ? folder.parent.path.formatPath() : null, + ); + }, + ), + SliverFixedExtentList.builder( + itemCount: folderTracks.length, + itemExtent: Dimensions.inst.trackTileItemExtent, itemBuilder: (context, i) { - final folder = mainMapFoldersKeys[i]; - if (folder.tracks.isEmpty) return const SizedBox(); - return FolderTile( - folder: folder, - subtitle: folder.hasSimilarFolderNames ? folder.parent.path.formatPath() : null, + final tr = folderTracks[i]; + return TrackTile( + index: i, + trackOrTwd: tr, + queueSource: QueueSource.folder, + bgColor: i == Folders.inst.indexToScrollTo.value ? highlighedColor : null, ); }, ), - SliverFixedExtentList.builder( - itemCount: Folders.inst.currentTracks.length, - itemExtent: Dimensions.inst.trackTileItemExtent, - itemBuilder: (context, i) { - final tr = Folders.inst.currentTracks[i]; - return TrackTile( - index: i, - trackOrTwd: tr, - queueSource: QueueSource.folder, - bgColor: i == Folders.inst.indexToScrollTo.value ? highlighedColor : null, - ); - }, - ), - kBottomPaddingWidgetSliver, - ], - ), + kBottomPaddingWidgetSliver, + scrollToiconBottomPaddingSliver, + ], + ); + }, ), ), ), diff --git a/lib/ui/pages/genres_page.dart b/lib/ui/pages/genres_page.dart index 8956ff0f..9cf28a9f 100644 --- a/lib/ui/pages/genres_page.dart +++ b/lib/ui/pages/genres_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/controller/scroll_search_controller.dart'; import 'package:namida/controller/search_sort_controller.dart'; @@ -47,24 +47,24 @@ class GenresPage extends StatelessWidget { ExpandableBox( enableHero: enableHero, gridWidget: ChangeGridCountWidget( - currentCount: settings.genreGridCount.value, + currentCount: settings.genreGridCount.valueR, onTap: () { final newCount = ScrollSearchController.inst.animateChangingGridSize(LibraryTab.genres, countPerRow, minimum: 2); settings.save(genreGridCount: newCount); }, ), - isBarVisible: LibraryTab.genres.isBarVisible, - showSearchBox: LibraryTab.genres.isSearchBoxVisible, + isBarVisible: LibraryTab.genres.isBarVisible.valueR, + showSearchBox: LibraryTab.genres.isSearchBoxVisible.valueR, leftText: SearchSortController.inst.genreSearchList.length.displayGenreKeyword, onFilterIconTap: () => ScrollSearchController.inst.switchSearchBoxVisibilty(LibraryTab.genres), onCloseButtonPressed: () => ScrollSearchController.inst.clearSearchTextField(LibraryTab.genres), sortByMenuWidget: SortByMenu( - title: settings.genreSort.value.toText(), + title: settings.genreSort.valueR.toText(), popupMenuChild: () => const SortByMenuGenres(), - isCurrentlyReversed: settings.genreSortReversed.value, + isCurrentlyReversed: settings.genreSortReversed.valueR, onReverseIconTap: () => SearchSortController.inst.sortMedia(MediaType.genre, reverse: !settings.genreSortReversed.value), ), - textField: CustomTextFiled( + textField: () => CustomTextFiled( textFieldController: LibraryTab.genres.textSearchController, textFieldHintText: lang.FILTER_GENRES, onTextFieldValueChanged: (value) => SearchSortController.inst.searchMedia(value, MediaType.genre), diff --git a/lib/ui/pages/home_page.dart b/lib/ui/pages/home_page.dart index 5b4f5143..e7469727 100644 --- a/lib/ui/pages/home_page.dart +++ b/lib/ui/pages/home_page.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:history_manager/history_manager.dart'; import 'package:jiffy/jiffy.dart'; @@ -137,22 +137,32 @@ class _HomePageState extends State with TickerProviderStateMixin, Pull ); // -- Lost Memories -- - final oldestYear = HistoryController.inst.oldestTrack?.dateAdded.milliSecondsSinceEpoch?.year; - final minusYearClamped = (timeNow.year - 1).withMinimum(oldestYear ?? 0); + final newestDaySinceEpoch = HistoryController.inst.historyMap.value.keys.firstOrNull; + final oldestDaySinceEpoch = HistoryController.inst.historyMap.value.keys.lastOrNull; + final newestYear = newestDaySinceEpoch == null ? 0 : DateTime.fromMillisecondsSinceEpoch(newestDaySinceEpoch * 24 * 60 * 60 * 1000).year; + final oldestYear = oldestDaySinceEpoch == null ? 0 : DateTime.fromMillisecondsSinceEpoch(oldestDaySinceEpoch * 24 * 60 * 60 * 1000).year; + + final minusYearClamped = (timeNow.year - 1).withMinimum(oldestYear); _updateSameTimeNYearsAgo(timeNow, minusYearClamped); + // -- Lost Memories Years + final diff = (newestYear - oldestYear).abs(); + for (int i = 1; i <= diff; i++) { + _lostMemoriesYears.add(newestYear - i); + } + // -- Recent Albums -- _recentAlbums.addAllIfEmpty(_recentListened.mappedUniqued((e) => e.track.albumIdentifier).take(25)); // -- Recent Artists -- _recentArtists.addAllIfEmpty(_recentListened.mappedUniquedList((e) => e.track.artistsList).take(25)); - _topRecentListened.loop((e, _) { + _topRecentListened.loop((e) { // -- Top Recent Albums -- _topRecentAlbums.update(e.key.albumIdentifier, (value) => value + 1, ifAbsent: () => 1); // -- Top Recent Artists -- - e.key.artistsList.loop((e, _) => _topRecentArtists.update(e, (value) => value + 1, ifAbsent: () => 1)); + e.key.artistsList.loop((e) => _topRecentArtists.update(e, (value) => value + 1, ifAbsent: () => 1)); }); _topRecentAlbums.sortByReverse((e) => e.value); _topRecentArtists.sortByReverse((e) => e.value); @@ -166,25 +176,23 @@ class _HomePageState extends State with TickerProviderStateMixin, Pull favs.shuffle(); // -- supermacy - final ct = Player.inst.nowPlayingTrack; - final maxCount = settings.queueInsertion[QueueInsertionType.algorithm]?.numberOfTracks ?? 25; - final sameAsCurrent = NamidaGenerator.inst.generateRecommendedTrack(ct).take(maxCount); - + final ct = Player.inst.currentTrack?.track; + final maxCount = settings.queueInsertion.value[QueueInsertionType.algorithm]?.numberOfTracks ?? 25; + MapEntry>? supremacyEntry; + if (ct != null) { + final sameAsCurrent = NamidaGenerator.inst.generateRecommendedTrack(ct).take(maxCount); + if (sameAsCurrent.isNotEmpty) { + final supremacy = [ct, ...sameAsCurrent]; + supremacyEntry = MapEntry('"${ct.title}" ${lang.SUPREMACY}', supremacy); + } + } _mixes.addAllIfEmpty([ MapEntry(lang.TOP_RECENTS, _topRecentListened.map((e) => e.key).toList()), - if (sameAsCurrent.isNotEmpty) MapEntry('"${ct.title}" ${lang.SUPREMACY}', [ct, ...sameAsCurrent]), + if (supremacyEntry != null) supremacyEntry, MapEntry(lang.FAVOURITES, favs.take(25).tracks.toList()), MapEntry(lang.RANDOM_PICKS, _randomTracks), ]); - final oldest = DateTime.fromMillisecondsSinceEpoch(HistoryController.inst.oldestTrack?.dateAdded ?? 0); - final newest = DateTime.fromMillisecondsSinceEpoch(HistoryController.inst.newestTrack?.dateAdded ?? 0); - - final diff = (newest.year - oldest.year).abs(); - for (int i = 1; i <= diff; i++) { - _lostMemoriesYears.add(newest.year - i); - } - _isLoading = false; if (mounted) setState(() {}); @@ -209,7 +217,7 @@ class _HomePageState extends State with TickerProviderStateMixin, Pull void showReorderHomeItemsDialog() async { final subList = [].obs; - HomePageItems.values.loop((e, index) { + HomePageItems.values.loop((e) { if (!settings.homePageItems.contains(e)) { subList.add(e); } @@ -217,7 +225,7 @@ class _HomePageState extends State with TickerProviderStateMixin, Pull final mainListController = ScrollController(); void jumpToLast() { mainListController.animateTo( - mainListController.position.maxScrollExtent, + mainListController.positions.first.maxScrollExtent, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, ); @@ -228,6 +236,7 @@ class _HomePageState extends State with TickerProviderStateMixin, Pull }); await NamidaNavigator.inst.navigateDialog( + scale: 1.0, onDisposing: () { subList.close(); mainListController.dispose(); @@ -241,48 +250,49 @@ class _HomePageState extends State with TickerProviderStateMixin, Pull ), ], child: SizedBox( - width: Get.width, - height: Get.height * 0.5, + width: namida.width, + height: namida.height * 0.5, child: Obx( () => Column( children: [ Expanded( flex: 6, - child: ReorderableListView.builder( - scrollController: mainListController, - itemCount: settings.homePageItems.length, - proxyDecorator: (child, index, animation) => child, - itemBuilder: (context, index) { - final item = settings.homePageItems[index]; - return Material( - key: ValueKey(index), - type: MaterialType.transparency, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ListTileWithCheckMark( - active: true, - icon: Broken.recovery_convert, - title: item.toText(), - onTap: () { - if (settings.homePageItems.length <= 3) { - showMinimumItemsSnack(3); - return; - } - subList.add(item); - settings.removeFromList(homePageItem1: item); - }, + child: Builder(builder: (context) { + return NamidaListView( + itemExtent: null, + scrollController: mainListController, + itemCount: settings.homePageItems.length, + itemBuilder: (context, index) { + final item = settings.homePageItems[index]; + return Material( + key: ValueKey(index), + type: MaterialType.transparency, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: ListTileWithCheckMark( + active: true, + icon: Broken.recovery_convert, + title: item.toText(), + onTap: () { + if (settings.homePageItems.length <= 3) { + showMinimumItemsSnack(3); + return; + } + subList.add(item); + settings.removeFromList(homePageItem1: item); + }, + ), ), - ), - ); - }, - onReorder: (oldIndex, newIndex) { - if (newIndex > oldIndex) newIndex -= 1; - - final item = settings.homePageItems.elementAt(oldIndex); - settings.removeFromList(homePageItem1: item); - settings.insertInList(newIndex, homePageItem1: item); - }, - ), + ); + }, + onReorder: (oldIndex, newIndex) { + if (newIndex > oldIndex) newIndex -= 1; + final item = settings.homePageItems.value.elementAt(oldIndex); + settings.removeFromList(homePageItem1: item); + settings.insertInList(newIndex, homePageItem1: item); + }, + ); + }), ), const NamidaContainerDivider(height: 4.0, margin: EdgeInsets.symmetric(vertical: 4.0)), if (subList.isNotEmpty) @@ -351,10 +361,9 @@ class _HomePageState extends State with TickerProviderStateMixin, Pull shimmerDelayMS: 250, shimmerEnabled: _isLoading, child: AnimationLimiter( - child: StreamBuilder>( - initialData: settings.homePageItems, - stream: settings.homePageItems.stream, - builder: (context, homePageItems) => CustomScrollView( + child: ObxO( + rx: settings.homePageItems, + builder: (homePageItems) => CustomScrollView( controller: _scrollController, slivers: [ const SliverPadding(padding: EdgeInsets.only(bottom: 12.0)), @@ -365,7 +374,7 @@ class _HomePageState extends State with TickerProviderStateMixin, Pull children: [ Text( 'Namida', - style: context.textTheme.displayLarge?.copyWith(fontSize: 32.0.multipliedFontScale), + style: context.textTheme.displayLarge?.copyWith(fontSize: 32.0), ), const Spacer(), NamidaIconButton( @@ -376,7 +385,7 @@ class _HomePageState extends State with TickerProviderStateMixin, Pull ), ), ), - ...homePageItems.data!.map( + ...homePageItems.map( (element) { switch (element) { case HomePageItems.mixes: @@ -914,7 +923,7 @@ class _MixesCardState extends State<_MixesCard> { Expanded( child: Text( widget.title, - style: context.textTheme.displayLarge?.copyWith(fontSize: 15.0.multipliedFontScale), + style: context.textTheme.displayLarge?.copyWith(fontSize: 15.0), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -937,7 +946,7 @@ class _MixesCardState extends State<_MixesCard> { const SizedBox(width: 4.0), Text( "${widget.tracks.length}", - style: context.textTheme.displayLarge?.copyWith(fontSize: 15.0.multipliedFontScale), + style: context.textTheme.displayLarge?.copyWith(fontSize: 15.0), ), ], ), @@ -1054,7 +1063,7 @@ class _MixesCardState extends State<_MixesCard> { clipBehavior: Clip.hardEdge, decoration: BoxDecoration( shape: BoxShape.circle, - color: context.theme.colorScheme.background.withAlpha(50), + color: context.theme.colorScheme.surface.withAlpha(50), ), child: NamidaBgBlur( blur: 2.0, @@ -1062,7 +1071,7 @@ class _MixesCardState extends State<_MixesCard> { padding: const EdgeInsets.all(2.0), child: NamidaIconButton( icon: Broken.arrow_left_2, - iconColor: context.theme.colorScheme.onBackground.withAlpha(160), + iconColor: context.theme.colorScheme.onSurface.withAlpha(160), onPressed: NamidaNavigator.inst.closeDialog, ), ), @@ -1092,7 +1101,7 @@ class _MixesCardState extends State<_MixesCard> { const SizedBox(width: 4.0), Text( "${widget.tracks.length}", - style: context.textTheme.displaySmall?.copyWith(fontSize: 15.0.multipliedFontScale), + style: context.textTheme.displaySmall?.copyWith(fontSize: 15.0), ), ], ), @@ -1159,7 +1168,7 @@ class _MixesCardState extends State<_MixesCard> { ), Text( widget.tracks.take(5).map((e) => e.title).join(', '), - style: context.textTheme.displaySmall?.copyWith(fontSize: 11.0.multipliedFontScale), + style: context.textTheme.displaySmall?.copyWith(fontSize: 11.0), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -1298,7 +1307,7 @@ class _TrackCardState extends State<_TrackCard> with LoadingItemsDelayMixin { child: Text( widget.topRightText!, style: context.textTheme.displaySmall?.copyWith( - fontSize: 10.5.multipliedFontScale, + fontSize: 10.5, fontWeight: FontWeight.w500, ), maxLines: 1, @@ -1331,13 +1340,13 @@ class _TrackCardState extends State<_TrackCard> with LoadingItemsDelayMixin { children: [ Text( track.title, - style: context.textTheme.displaySmall?.copyWith(fontSize: 12.0.multipliedFontScale, fontWeight: FontWeight.w500), + style: context.textTheme.displaySmall?.copyWith(fontSize: 12.0, fontWeight: FontWeight.w500), maxLines: 1, overflow: TextOverflow.ellipsis, ), Text( track.originalArtist, - style: context.textTheme.displaySmall?.copyWith(fontSize: 11.0.multipliedFontScale, fontWeight: FontWeight.w400), + style: context.textTheme.displaySmall?.copyWith(fontSize: 11.0, fontWeight: FontWeight.w400), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -1372,13 +1381,12 @@ class RecentlyAddedTracksPage extends StatelessWidget { const SizedBox(width: 12.0), Text( lang.RECENTLY_ADDED, - style: context.textTheme.displayLarge?.copyWith(fontSize: 18.0.multipliedFontScale), + style: context.textTheme.displayLarge?.copyWith(fontSize: 18.0), ) ], ), ), queueLength: tracksSorted.length, - isTrackSelectable: true, queueSource: QueueSource.recentlyAdded, queue: tracksSorted, thirdLineText: (track) { diff --git a/lib/ui/pages/main_page.dart b/lib/ui/pages/main_page.dart index 3ddc1793..110aed59 100644 --- a/lib/ui/pages/main_page.dart +++ b/lib/ui/pages/main_page.dart @@ -1,9 +1,7 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; +import 'package:namida/class/track.dart'; import 'package:namida/controller/clipboard_controller.dart'; import 'package:namida/controller/current_color.dart'; import 'package:namida/controller/navigator_controller.dart'; @@ -20,6 +18,7 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/main.dart'; import 'package:namida/packages/searchbar_animation.dart'; import 'package:namida/ui/pages/albums_page.dart'; @@ -51,7 +50,9 @@ class MainPage extends StatelessWidget { WidgetsBinding.instance.addPostFrameCallback((_) { NamidaNavigator.inst.onFirstLoad(); }); - return GetPageRoute(page: () => const SizedBox()); + return MaterialPageRoute( + builder: (_) => const SizedBox(), + ); }, ), ); @@ -99,28 +100,29 @@ class MainPage extends StatelessWidget { child: Obx( () => AnimatedSwitcher( duration: const Duration(milliseconds: 400), - child: ScrollSearchController.inst.isGlobalSearchMenuShown.value ? const SearchPage() : null, + child: ScrollSearchController.inst.isGlobalSearchMenuShown.valueR ? const SearchPage() : null, ), ), ), // -- Settings Search Box Positioned.fill( - child: Obx( - () => AnimatedSwitcher( + child: ObxO( + rx: SettingsSearchController.inst.canShowSearch, + builder: (canShowSearch) => AnimatedSwitcher( duration: const Duration(milliseconds: 400), - child: SettingsSearchController.inst.canShowSearch ? const SettingsSearchPage() : null, + child: canShowSearch ? const SettingsSearchPage() : null, ), ), ), Obx( () { - final shouldHide = Dimensions.inst.shouldHideFAB; + final shouldHide = Dimensions.inst.shouldHideFABR; return AnimatedPositioned( key: const Key('fab_active'), right: 12.0, - bottom: fabBottomOffset.withMinimum(Dimensions.inst.globalBottomPaddingEffective), + bottom: fabBottomOffset.withMinimum(Dimensions.inst.globalBottomPaddingEffectiveR), duration: const Duration(milliseconds: 300), curve: Curves.fastEaseInToSlowEaseOut, child: AnimatedSwitcher( @@ -129,7 +131,7 @@ class MainPage extends StatelessWidget { ? const SizedBox(key: Key('fab_dummy')) : FloatingActionButton( heroTag: 'main_page_fab_hero', - tooltip: ScrollSearchController.inst.isGlobalSearchMenuShown.value ? lang.CLEAR : settings.floatingActionButton.value.toText(), + tooltip: ScrollSearchController.inst.isGlobalSearchMenuShown.valueR ? lang.CLEAR : settings.floatingActionButton.valueR.toText(), backgroundColor: Color.alphaBlend(CurrentColor.inst.currentColorScheme.withOpacity(0.6), context.theme.cardColor), onPressed: () { final fab = settings.floatingActionButton.value; @@ -146,10 +148,10 @@ class MainPage extends StatelessWidget { ScrollSearchController.inst.searchBarKey.currentState?.openCloseSearchBar(); } } else if (fab == FABType.shuffle || fab == FABType.play) { - Player.inst.playOrPause(0, SelectedTracksController.inst.currentAllTracks, QueueSource.allTracks, shuffle: fab == FABType.shuffle); + Player.inst.playOrPause(0, SelectedTracksController.inst.getCurrentAllTracks(), QueueSource.allTracks, shuffle: fab == FABType.shuffle); } }, - child: ScrollSearchController.inst.isGlobalSearchMenuShown.value + child: ScrollSearchController.inst.isGlobalSearchMenuShown.valueR ? Stack( alignment: Alignment.center, children: [ @@ -161,7 +163,7 @@ class MainPage extends StatelessWidget { ], ) : Icon( - settings.floatingActionButton.value.toIcon(), + settings.floatingActionButton.valueR.toIcon(), color: const Color.fromRGBO(255, 255, 255, 0.8), ), ), @@ -172,31 +174,34 @@ class MainPage extends StatelessWidget { /// Bottom Glow/Shadow Obx( - () => AnimatedSwitcher( - duration: const Duration(milliseconds: 600), - child: Player.inst.currentQueue.isNotEmpty || (Player.inst.currentQueueYoutube.isNotEmpty && !settings.youtubeStyleMiniplayer.value) - ? SizedBox( - key: const Key('actualglow'), - height: 28.0, - width: context.width, - child: Transform( - transform: Matrix4.translationValues(0, 8.0, 0), - child: AnimatedDecoration( - duration: const Duration(milliseconds: kThemeAnimationDurationMS), - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: context.theme.scaffoldBackgroundColor, - spreadRadius: 4.0, - blurRadius: 8.0, - ), - ], + () { + final currentItem = Player.inst.currentItem.valueR; + return AnimatedSwitcher( + duration: const Duration(milliseconds: 600), + child: currentItem is Selectable || (currentItem is YoutubeID && !settings.youtubeStyleMiniplayer.valueR) + ? SizedBox( + key: const Key('actualglow'), + height: 28.0, + width: context.width, + child: Transform( + transform: Matrix4.translationValues(0, 8.0, 0), + child: AnimatedDecoration( + duration: const Duration(milliseconds: kThemeAnimationDurationMS), + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: context.theme.scaffoldBackgroundColor, + spreadRadius: 4.0, + blurRadius: 8.0, + ), + ], + ), ), ), - ), - ) - : const SizedBox(key: Key('emptyglow')), - ), + ) + : const SizedBox(key: Key('emptyglow')), + ); + }, ) ], ), @@ -206,19 +211,24 @@ class MainPage extends StatelessWidget { final theme = context.theme; + final animatedThemeWidget = _AnimatedTheme( + key: _animatedThemeGlobalKey, + duration: const Duration(milliseconds: kThemeAnimationDurationMS), + data: theme, + child: mainChild, + ); + final animatedThemeState = _animatedThemeGlobalKey.currentState; + animatedThemeState?.setAnimated(animation.value < 1); + return AnimatedBuilder( animation: animation, builder: (context, _) { final mainPlayerVisible = animation.value < 1; + animatedThemeState?.setAnimated(mainPlayerVisible); return Visibility( maintainState: true, visible: mainPlayerVisible, - child: _AnimatedTheme( - duration: const Duration(milliseconds: kThemeAnimationDurationMS), - data: theme, - animated: mainPlayerVisible, - child: mainChild, - ), + child: animatedThemeWidget, ); }, ); @@ -263,12 +273,12 @@ class NamidaSearchBar extends StatelessWidget { enableBoxShadow: false, buttonShadowColour: Colors.transparent, hintTextStyle: (height) => context.textTheme.displaySmall?.copyWith( - fontSize: 17.0.multipliedFontScale, + fontSize: 17.0, height: height * 1.1, ), searchBoxColour: context.theme.cardColor.withAlpha(200), enteredTextStyle: context.theme.textTheme.displayMedium, - cursorColour: context.theme.colorScheme.onBackground, + cursorColour: context.theme.colorScheme.onSurface, buttonBorderColour: Colors.black45, cursorRadius: const Radius.circular(12.0), buttonWidget: const IgnorePointer( @@ -284,9 +294,9 @@ class NamidaSearchBar extends StatelessWidget { buttonWidgetSmallPadding: 24.0 + 8.0, buttonWidgetSmall: Obx( () { - final clipboard = ClipboardController.inst.clipboardText; - final alreadyPasted = clipboard == ClipboardController.inst.lastCopyUsed; - final empty = ClipboardController.inst.textInControllerEmpty; + final clipboard = ClipboardController.inst.clipboardText.valueR; + final alreadyPasted = clipboard == ClipboardController.inst.lastCopyUsed.valueR; + final empty = ClipboardController.inst.textInControllerEmpty.valueR; return AnimatedSwitcher( duration: const Duration(milliseconds: 200), @@ -296,7 +306,7 @@ class NamidaSearchBar extends StatelessWidget { icon: Broken.clipboard_tick, iconSize: 20.0, onPressed: () { - ClipboardController.inst.setLastPasted(clipboard); + ClipboardController.inst.setLastPasted(ClipboardController.inst.clipboardText.value); final c = ScrollSearchController.inst.searchTextEditingController; c.text = "${c.text} $clipboard"; c.selection = TextSelection.fromPosition(TextPosition(offset: c.text.length)); @@ -334,11 +344,8 @@ class NamidaSearchBar extends StatelessWidget { onFieldSubmitted: _onSubmitted, onChanged: (value) { if (ScrollSearchController.inst.currentSearchType.value == SearchType.localTracks) { - _searchFieldTimer?.cancel(); - _searchFieldTimer = Timer(const Duration(milliseconds: 150), () { - ClipboardController.inst.updateTextInControllerEmpty(value == ''); - SearchSortController.inst.searchAll(value); - }); + ClipboardController.inst.updateTextInControllerEmpty(value == ''); + SearchSortController.inst.searchAll(value); } }, // -- unfocusing produces weird bug while swiping for drawer @@ -349,15 +356,13 @@ class NamidaSearchBar extends StatelessWidget { } } -Timer? _searchFieldTimer; - class AlbumSearchResultsPage extends StatelessWidget { const AlbumSearchResultsPage({super.key}); @override Widget build(BuildContext context) { return AlbumsPage( - albumIdentifiers: SearchSortController.inst.albumSearchTemp, + albumIdentifiers: SearchSortController.inst.albumSearchTemp.value, countPerRow: settings.albumGridCount.value, ); } @@ -404,8 +409,9 @@ class _CustomAppBar extends StatelessWidget { final overlayStyle = _systemOverlayStyleForBrightness(ThemeData.estimateBrightnessForColor(backgroundColor), theme.useMaterial3 ? const Color(0x00000000) : null); final appbar = Obx( () { - final title = ScrollSearchController.inst.isGlobalSearchMenuShown.value ? ScrollSearchController.inst.searchBarWidget : NamidaNavigator.inst.currentRoute?.toTitle(context); - final actions = NamidaNavigator.inst.currentRoute?.toActions(); + final title = + ScrollSearchController.inst.isGlobalSearchMenuShown.valueR ? ScrollSearchController.inst.searchBarWidget : NamidaNavigator.inst.currentRouteR?.toTitle(context); + final actions = NamidaNavigator.inst.currentRouteR?.toActions(); return Row( children: [ ConstrainedBox( @@ -442,7 +448,7 @@ class _CustomAppBar extends StatelessWidget { bottom: false, child: Obx( () { - return !settings.enableMiniplayerParallaxEffect.value + return !settings.enableMiniplayerParallaxEffect.valueR ? SizedBox( height: kToolbarHeight, child: appbar, @@ -475,10 +481,10 @@ class _CustomNavBar extends StatelessWidget { data: NavigationBarThemeData( backgroundColor: context.theme.navigationBarTheme.backgroundColor, indicatorColor: Color.alphaBlend(context.theme.colorScheme.primary.withAlpha(20), context.theme.colorScheme.secondaryContainer), - labelTextStyle: MaterialStatePropertyAll( + labelTextStyle: const WidgetStatePropertyAll( TextStyle( overflow: TextOverflow.ellipsis, - fontSize: 13.0.multipliedFontScale, + fontSize: 13.0, ), ), ), @@ -488,13 +494,13 @@ class _CustomNavBar extends StatelessWidget { elevation: 22, labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected, height: 64.0, - onDestinationSelected: (value) async { - final tab = value.toEnum(); + onDestinationSelected: (destinationIndex) { + final tab = settings.libraryTabs.value[destinationIndex]; ScrollSearchController.inst.animatePageController(tab); }, - selectedIndex: settings.selectedLibraryTab.value.toInt().toIf(0, -1), + selectedIndex: settings.selectedLibraryTab.valueR.toInt().toIf(0, -1), destinations: [ - ...settings.libraryTabs.map( + ...settings.libraryTabs.valueR.map( (e) => NavigationDestination( icon: Icon(e.toIcon()), label: settings.libraryTabs.length >= 7 ? '' : e.toText(), @@ -506,7 +512,7 @@ class _CustomNavBar extends StatelessWidget { ); return Obx( - () => !settings.enableBottomNavBar.value + () => !settings.enableBottomNavBar.valueR ? const SizedBox() : AnimatedBuilder( animation: animation, @@ -521,17 +527,18 @@ class _CustomNavBar extends StatelessWidget { } } +final _animatedThemeGlobalKey = GlobalKey<_AnimatedThemeState>(); + class _AnimatedTheme extends ImplicitlyAnimatedWidget { const _AnimatedTheme({ + required super.key, required this.data, required this.child, - required this.animated, required super.duration, }); final ThemeData data; final Widget child; - final bool animated; @override AnimatedWidgetBaseState<_AnimatedTheme> createState() => _AnimatedThemeState(); @@ -539,20 +546,29 @@ class _AnimatedTheme extends ImplicitlyAnimatedWidget { class _AnimatedThemeState extends AnimatedWidgetBaseState<_AnimatedTheme> { ThemeDataTween? _data; + bool _animated = true; + bool _themeDidChange = true; + + void setAnimated(bool animated) { + if (animated != _animated) { + refreshState(() { + _themeDidChange = false; + _animated = animated; + }); + } + } @override void forEachTween(TweenVisitor visitor) { - if (!widget.animated) { - _data = null; - return; - } + if (!_animated) return; + _themeDidChange = true; _data = visitor(_data, widget.data, (dynamic value) => ThemeDataTween(begin: value as ThemeData))! as ThemeDataTween; } @override Widget build(BuildContext context) { return Theme( - data: widget.animated ? _data?.evaluate(animation) ?? widget.data : widget.data, + data: !_animated || !_themeDidChange ? widget.data : _data?.evaluate(animation) ?? widget.data, child: widget.child, ); } diff --git a/lib/ui/pages/onboarding.dart b/lib/ui/pages/onboarding.dart index fa2cdd58..0291fe71 100644 --- a/lib/ui/pages/onboarding.dart +++ b/lib/ui/pages/onboarding.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/controller/backup_controller.dart'; import 'package:namida/controller/current_color.dart'; @@ -188,7 +188,7 @@ class _FirstRunConfigureScreenState extends State { decoration: BoxDecoration( boxShadow: [ BoxShadow( - color: _shouldShowGlow.value ? CurrentColor.inst.color : Colors.transparent, + color: _shouldShowGlow.valueR ? CurrentColor.inst.color : Colors.transparent, blurRadius: 12.0, spreadRadius: 2.0, ) diff --git a/lib/ui/pages/playlists_page.dart b/lib/ui/pages/playlists_page.dart index f1ca4a4e..50d6e1ff 100644 --- a/lib/ui/pages/playlists_page.dart +++ b/lib/ui/pages/playlists_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/base/pull_to_refresh.dart'; import 'package:namida/class/track.dart'; @@ -87,24 +87,24 @@ class _PlaylistsPageState extends State with TickerProviderStateM gridWidget: isInsideDialog ? null : ChangeGridCountWidget( - currentCount: settings.playlistGridCount.value, + currentCount: settings.playlistGridCount.valueR, onTap: () { final newCount = ScrollSearchController.inst.animateChangingGridSize(LibraryTab.playlists, widget.countPerRow); settings.save(playlistGridCount: newCount); }, ), - isBarVisible: LibraryTab.playlists.isBarVisible, - showSearchBox: LibraryTab.playlists.isSearchBoxVisible, + isBarVisible: LibraryTab.playlists.isBarVisible.valueR, + showSearchBox: LibraryTab.playlists.isSearchBoxVisible.valueR, leftText: SearchSortController.inst.playlistSearchList.length.displayPlaylistKeyword, onFilterIconTap: () => ScrollSearchController.inst.switchSearchBoxVisibilty(LibraryTab.playlists), onCloseButtonPressed: () => ScrollSearchController.inst.clearSearchTextField(LibraryTab.playlists), sortByMenuWidget: SortByMenu( - title: settings.playlistSort.value.toText(), + title: settings.playlistSort.valueR.toText(), popupMenuChild: () => const SortByMenuPlaylist(), - isCurrentlyReversed: settings.playlistSortReversed.value, + isCurrentlyReversed: settings.playlistSortReversed.valueR, onReverseIconTap: () => SearchSortController.inst.sortMedia(MediaType.playlist, reverse: !settings.playlistSortReversed.value), ), - textField: CustomTextFiled( + textField: () => CustomTextFiled( textFieldController: LibraryTab.playlists.textSearchController, textFieldHintText: lang.FILTER_PLAYLISTS, onTextFieldValueChanged: (value) => SearchSortController.inst.searchMedia(value, MediaType.playlist), @@ -165,13 +165,14 @@ class _PlaylistsPageState extends State with TickerProviderStateM Expanded( child: NamidaHero( tag: 'DPC_history', - child: Obx( - () => DefaultPlaylistCard( + child: ObxO( + rx: HistoryController.inst.totalHistoryItemsCount, + builder: (count) => DefaultPlaylistCard( colorScheme: Colors.grey, icon: Broken.refresh, title: lang.HISTORY, - displayLoadingIndicator: HistoryController.inst.isLoadingHistory, - text: HistoryController.inst.historyTracksLength.formatDecimal(), + displayLoadingIndicator: count == -1, + text: count.formatDecimal(), onTap: () => NamidaOnTaps.inst.onHistoryPlaylistTap(), ), ), @@ -186,7 +187,7 @@ class _PlaylistsPageState extends State with TickerProviderStateM colorScheme: Colors.green, icon: Broken.award, title: lang.MOST_PLAYED, - displayLoadingIndicator: HistoryController.inst.isLoadingHistory, + displayLoadingIndicator: HistoryController.inst.isLoadingHistoryR, text: HistoryController.inst.topTracksMapListens.length.formatDecimal(), onTap: () => NamidaOnTaps.inst.onMostPlayedPlaylistTap(), ), @@ -208,7 +209,7 @@ class _PlaylistsPageState extends State with TickerProviderStateM colorScheme: Colors.red, icon: Broken.heart, title: lang.FAVOURITES, - text: PlaylistController.inst.favouritesPlaylist.value.tracks.length.formatDecimal(), + text: PlaylistController.inst.favouritesPlaylist.valueR.tracks.length.formatDecimal(), onTap: () => NamidaOnTaps.inst.onNormalPlaylistTap(k_PLAYLIST_NAME_FAV), ), ), @@ -224,7 +225,7 @@ class _PlaylistsPageState extends State with TickerProviderStateM icon: Broken.driver, title: lang.QUEUES, displayLoadingIndicator: QueueController.inst.isLoadingQueues, - text: QueueController.inst.queuesMap.value.length.formatDecimal(), + text: QueueController.inst.queuesMap.valueR.length.formatDecimal(), onTap: () => NamidaNavigator.inst.navigateTo(const QueuesPage()), ), ), diff --git a/lib/ui/pages/queues_page.dart b/lib/ui/pages/queues_page.dart index 918ddd27..abad1d1b 100644 --- a/lib/ui/pages/queues_page.dart +++ b/lib/ui/pages/queues_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/controller/queue_controller.dart'; import 'package:namida/core/dimensions.dart'; @@ -10,18 +10,19 @@ import 'package:namida/ui/widgets/library/queue_tile.dart'; class QueuesPage extends StatelessWidget { const QueuesPage({super.key}); + @override Widget build(BuildContext context) { - return AnimationLimiter( - child: BackgroundWrapper( - child: NamidaScrollbarWithController( - child: (sc) => CustomScrollView( + return BackgroundWrapper( + child: NamidaScrollbarWithController( + child: (sc) => AnimationLimiter( + child: CustomScrollView( controller: sc, slivers: [ const SliverPadding(padding: EdgeInsets.only(top: Dimensions.tileBottomMargin6)), Obx( () { - final queuesKeys = QueueController.inst.queuesMap.value.keys.toList(); + final queuesKeys = QueueController.inst.queuesMap.valueR.keys.toList(); final queuesLength = queuesKeys.length; return SliverFixedExtentList.builder( itemCount: queuesLength, diff --git a/lib/ui/pages/search_page.dart b/lib/ui/pages/search_page.dart index 61185f89..77029f87 100644 --- a/lib/ui/pages/search_page.dart +++ b/lib/ui/pages/search_page.dart @@ -2,7 +2,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/folder.dart'; import 'package:namida/class/track.dart'; @@ -149,7 +149,7 @@ class SearchPage extends StatelessWidget { ...MediaType.values.map( (e) => Obx( () { - final list = settings.activeSearchMediaTypes; + final list = settings.activeSearchMediaTypes.valueR; final isActive = list.contains(e); final isForcelyEnabled = e == MediaType.track; return NamidaOpacity( @@ -213,7 +213,7 @@ class SearchPage extends StatelessWidget { child: NamidaOpacity( opacity: 0.8, child: TweenAnimationBuilder( - tween: Tween(begin: 4.0, end: ScrollSearchController.inst.isGlobalSearchMenuShown.value ? 4.0 : 12.0), + tween: Tween(begin: 4.0, end: ScrollSearchController.inst.isGlobalSearchMenuShown.valueR ? 4.0 : 12.0), duration: const Duration(milliseconds: 500), child: Image.asset('assets/namida_icon.png', cacheHeight: 100, scale: 0.4), builder: (context, value, child) => ImageFiltered( @@ -226,20 +226,20 @@ class SearchPage extends StatelessWidget { ), ), ) - : AnimationLimiter( + : NamidaScrollbarWithController( key: const Key('fullsearch'), - child: NamidaScrollbarWithController( - child: (sc) => Obx( + child: (sc) => AnimationLimiter( + child: Obx( () { - final activeList = settings.activeSearchMediaTypes; + final activeList = settings.activeSearchMediaTypes.valueR; - final albumSearchTemp = SearchSortController.inst.albumSearchTemp; - final artistSearchTemp = SearchSortController.inst.artistSearchTemp; - final albumArtistSearchTemp = SearchSortController.inst.albumArtistSearchTemp; - final composerSearchTemp = SearchSortController.inst.composerSearchTemp; - final genreSearchTemp = SearchSortController.inst.genreSearchTemp; - final playlistSearchTemp = SearchSortController.inst.playlistSearchTemp; - final folderSearchTemp = SearchSortController.inst.folderSearchTemp.where((f) => Folder(f).tracks.isNotEmpty).toList(); + final albumSearchTemp = SearchSortController.inst.albumSearchTemp.valueR; + final artistSearchTemp = SearchSortController.inst.artistSearchTemp.valueR; + final albumArtistSearchTemp = SearchSortController.inst.albumArtistSearchTemp.valueR; + final composerSearchTemp = SearchSortController.inst.composerSearchTemp.valueR; + final genreSearchTemp = SearchSortController.inst.genreSearchTemp.valueR; + final playlistSearchTemp = SearchSortController.inst.playlistSearchTemp.valueR; + final folderSearchTemp = SearchSortController.inst.folderSearchTemp.valueR.where((f) => Folder(f).tracks().isNotEmpty).toList(); return CustomScrollView( controller: sc, @@ -386,7 +386,7 @@ class SearchPage extends StatelessWidget { list: folderSearchTemp, builder: (item) { final folder = Folder(item); - final tracks = folder.tracks; + final tracks = folder.tracks(); return NamidaInkWell( margin: const EdgeInsets.only(left: 6.0), @@ -413,13 +413,13 @@ class SearchPage extends StatelessWidget { Text( folder.folderName, style: context.textTheme.displayMedium?.copyWith( - fontSize: 13.0.multipliedFontScale, + fontSize: 13.0, ), ), Text( tracks.length.displayTrackKeyword, style: context.textTheme.displaySmall?.copyWith( - fontSize: 12.0.multipliedFontScale, + fontSize: 12.0, ), ), ], @@ -449,8 +449,8 @@ class SearchPage extends StatelessWidget { child: NamidaInkWell( child: Obx( () { - final isAuto = settings.tracksSortSearchIsAuto.value; - final activeType = isAuto ? settings.tracksSort.value : settings.tracksSortSearch.value; + final isAuto = settings.tracksSortSearchIsAuto.valueR; + final activeType = isAuto ? settings.tracksSort.valueR : settings.tracksSortSearch.valueR; return Text( activeType.toText() + (isAuto ? ' (${lang.AUTO})' : ''), style: context.textTheme.displaySmall?.copyWith( @@ -469,8 +469,8 @@ class SearchPage extends StatelessWidget { }, child: Obx( () { - final isAuto = settings.tracksSortSearchIsAuto.value; - final activeReverse = isAuto ? settings.tracksSortReversed.value : settings.tracksSortSearchReversed.value; + final isAuto = settings.tracksSortSearchIsAuto.valueR; + final activeReverse = isAuto ? settings.tracksSortReversed.valueR : settings.tracksSortSearchReversed.valueR; return Icon( activeReverse ? Broken.arrow_up_3 : Broken.arrow_down_2, size: 16.0, @@ -482,7 +482,7 @@ class SearchPage extends StatelessWidget { ], ), buttonIcon: Broken.play, - buttonText: settings.trackPlayMode.value.toText(), + buttonText: settings.trackPlayMode.valueR.toText(), onPressed: () { final element = settings.trackPlayMode.value.nextElement(TrackPlayMode.values); settings.save(trackPlayMode: element); diff --git a/lib/ui/pages/settings_page.dart b/lib/ui/pages/settings_page.dart index 00a36a50..ace1ff84 100644 --- a/lib/ui/pages/settings_page.dart +++ b/lib/ui/pages/settings_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/controller/current_color.dart'; import 'package:namida/controller/navigator_controller.dart'; @@ -106,68 +106,64 @@ class CollapsedSettingTiles extends StatelessWidget { @override Widget build(BuildContext context) { - return Obx( - () { - settings.selectedLanguage.value; - return ListView( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - children: [ - CustomCollapsedListTile( - title: lang.THEME_SETTINGS, - subtitle: lang.THEME_SETTINGS_SUBTITLE, - icon: Broken.brush_2, - page: const ThemeSetting(), - ), - CustomCollapsedListTile( - title: lang.INDEXER, - subtitle: lang.INDEXER_SUBTITLE, - icon: Broken.component, - page: const IndexerSettings(), - trailing: const IndexingPercentage(size: 32.0), - ), - CustomCollapsedListTile( - title: lang.PLAYBACK_SETTING, - subtitle: lang.PLAYBACK_SETTING_SUBTITLE, - icon: Broken.play_cricle, - page: const PlaybackSettings(), - ), - CustomCollapsedListTile( - title: lang.CUSTOMIZATIONS, - subtitle: lang.CUSTOMIZATIONS_SUBTITLE, - icon: Broken.brush_1, - page: const CustomizationSettings(), - ), - CustomCollapsedListTile( - title: lang.YOUTUBE, - subtitle: lang.YOUTUBE_SETTINGS_SUBTITLE, - icon: Broken.video, - page: const YoutubeSettings(), - ), - CustomCollapsedListTile( - title: lang.EXTRAS, - subtitle: lang.EXTRAS_SUBTITLE, - icon: Broken.command_square, - page: const ExtrasSettings(), - ), - CustomCollapsedListTile( - title: lang.BACKUP_AND_RESTORE, - subtitle: lang.BACKUP_AND_RESTORE_SUBTITLE, - icon: Broken.refresh_circle, - page: const BackupAndRestore(), - trailing: const ParsingJsonPercentage(size: 32.0), - ), - CustomCollapsedListTile( - title: lang.ADVANCED_SETTINGS, - subtitle: lang.ADVANCED_SETTINGS_SUBTITLE, - icon: Broken.hierarchy_3, - page: const AdvancedSettings(), - ), - const AboutPageTileWidget(), - const CollapsedSettingTileWidget(), - kBottomPaddingWidget, - ], - ); - }, + Localizations.localeOf(context); + return ListView( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + children: [ + CustomCollapsedListTile( + title: lang.THEME_SETTINGS, + subtitle: lang.THEME_SETTINGS_SUBTITLE, + icon: Broken.brush_2, + page: const ThemeSetting(), + ), + CustomCollapsedListTile( + title: lang.INDEXER, + subtitle: lang.INDEXER_SUBTITLE, + icon: Broken.component, + page: const IndexerSettings(), + trailing: const IndexingPercentage(size: 32.0), + ), + CustomCollapsedListTile( + title: lang.PLAYBACK_SETTING, + subtitle: lang.PLAYBACK_SETTING_SUBTITLE, + icon: Broken.play_cricle, + page: const PlaybackSettings(), + ), + CustomCollapsedListTile( + title: lang.CUSTOMIZATIONS, + subtitle: lang.CUSTOMIZATIONS_SUBTITLE, + icon: Broken.brush_1, + page: const CustomizationSettings(), + ), + CustomCollapsedListTile( + title: lang.YOUTUBE, + subtitle: lang.YOUTUBE_SETTINGS_SUBTITLE, + icon: Broken.video, + page: const YoutubeSettings(), + ), + CustomCollapsedListTile( + title: lang.EXTRAS, + subtitle: lang.EXTRAS_SUBTITLE, + icon: Broken.command_square, + page: const ExtrasSettings(), + ), + CustomCollapsedListTile( + title: lang.BACKUP_AND_RESTORE, + subtitle: lang.BACKUP_AND_RESTORE_SUBTITLE, + icon: Broken.refresh_circle, + page: const BackupAndRestore(), + trailing: const ParsingJsonPercentage(size: 32.0), + ), + CustomCollapsedListTile( + title: lang.ADVANCED_SETTINGS, + subtitle: lang.ADVANCED_SETTINGS_SUBTITLE, + icon: Broken.hierarchy_3, + page: const AdvancedSettings(), + ), + const AboutPageTileWidget(), + const CollapsedSettingTileWidget(), + kBottomPaddingWidget, + ], ); } } diff --git a/lib/ui/pages/settings_search_page.dart b/lib/ui/pages/settings_search_page.dart index 08b64077..1ea029d2 100644 --- a/lib/ui/pages/settings_search_page.dart +++ b/lib/ui/pages/settings_search_page.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/controller/settings_search_controller.dart'; import 'package:namida/core/dimensions.dart'; diff --git a/lib/ui/pages/subpages/album_tracks_subpage.dart b/lib/ui/pages/subpages/album_tracks_subpage.dart index b0005d17..b740fa41 100644 --- a/lib/ui/pages/subpages/album_tracks_subpage.dart +++ b/lib/ui/pages/subpages/album_tracks_subpage.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:namida/core/dimensions.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/track.dart'; import 'package:namida/controller/indexer_controller.dart'; @@ -25,24 +26,25 @@ class AlbumTracksPage extends StatelessWidget { @override Widget build(BuildContext context) { final name = tracks.album; + final displayTrackNumberinAlbumPage = settings.displayTrackNumberinAlbumPage.value; return BackgroundWrapper( child: Obx( () { - Indexer.inst.mainMapAlbums.value; // to update after sorting + Indexer.inst.mainMapAlbums.valueR; // to update after sorting return NamidaTracksList( queueSource: QueueSource.album, queueLength: tracks.length, queue: tracks, - displayTrackNumber: settings.displayTrackNumberinAlbumPage.value, + displayTrackNumber: displayTrackNumberinAlbumPage, header: SubpagesTopContainer( title: name, source: QueueSource.album, subtitle: [tracks.displayTrackKeyword, tracks.totalDurationFormatted].join(' - '), thirdLineText: tracks.albumArtist, heroTag: 'album_$albumIdentifier', - imageWidget: shouldAlbumBeSquared + imageWidget: Dimensions.inst.shouldAlbumBeSquared // non reactive ? MultiArtworkContainer( - size: Get.width * 0.35, + size: namida.width * 0.35, heroTag: 'album_$albumIdentifier', tracks: [tracks.trackOfImage ?? kDummyTrack], ) @@ -54,7 +56,7 @@ class AlbumTracksPage extends StatelessWidget { child: ArtworkWidget( key: Key(tracks.pathToImage), track: tracks.trackOfImage, - thumbnailSize: Get.width * 0.35, + thumbnailSize: namida.width * 0.35, forceSquared: false, path: tracks.pathToImage, compressed: false, @@ -62,7 +64,7 @@ class AlbumTracksPage extends StatelessWidget { ), ), ), - tracks: tracks, + tracksFn: () => tracks, ), ); }, diff --git a/lib/ui/pages/subpages/artist_tracks_subpage.dart b/lib/ui/pages/subpages/artist_tracks_subpage.dart index ba9737ca..01dd703e 100644 --- a/lib/ui/pages/subpages/artist_tracks_subpage.dart +++ b/lib/ui/pages/subpages/artist_tracks_subpage.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/track.dart'; import 'package:namida/controller/indexer_controller.dart'; @@ -36,11 +36,11 @@ class ArtistTracksPage extends StatelessWidget { () { // to update after sorting type == MediaType.artist - ? Indexer.inst.mainMapArtists.value + ? Indexer.inst.mainMapArtists.valueR : type == MediaType.albumArtist - ? Indexer.inst.mainMapAlbumArtists.value + ? Indexer.inst.mainMapAlbumArtists.valueR : type == MediaType.composer - ? Indexer.inst.mainMapComposer.value + ? Indexer.inst.mainMapComposer.valueR : null; return NamidaTracksList( queueSource: QueueSource.artist, @@ -67,7 +67,7 @@ class ArtistTracksPage extends StatelessWidget { child: ArtworkWidget( key: Key(tracks.pathToImage), track: tracks.trackOfImage, - thumbnailSize: Get.width * 0.35, + thumbnailSize: namida.width * 0.35, path: tracks.pathToImage, forceSquared: true, blur: 0, @@ -76,7 +76,7 @@ class ArtistTracksPage extends StatelessWidget { ), ), ), - tracks: tracks, + tracksFn: () => tracks, ), NamidaExpansionTile( icon: Broken.music_dashboard, diff --git a/lib/ui/pages/subpages/genre_tracks_subpage.dart b/lib/ui/pages/subpages/genre_tracks_subpage.dart index 84b94109..d7436aea 100644 --- a/lib/ui/pages/subpages/genre_tracks_subpage.dart +++ b/lib/ui/pages/subpages/genre_tracks_subpage.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/track.dart'; import 'package:namida/controller/indexer_controller.dart'; @@ -22,7 +22,7 @@ class GenreTracksPage extends StatelessWidget { return BackgroundWrapper( child: Obx( () { - Indexer.inst.mainMapGenres.value; // to update after sorting + Indexer.inst.mainMapGenres.valueR; // to update after sorting return NamidaTracksList( queueSource: QueueSource.genre, queueLength: tracks.length, @@ -33,11 +33,11 @@ class GenreTracksPage extends StatelessWidget { subtitle: [tracks.displayTrackKeyword, tracks.totalDurationFormatted].join(' - '), heroTag: 'genre_$name', imageWidget: MultiArtworkContainer( - size: Get.width * 0.35, + size: namida.width * 0.35, heroTag: 'genre_$name', tracks: tracks.toImageTracks(), ), - tracks: tracks, + tracksFn: () => tracks, ), ); }, diff --git a/lib/ui/pages/subpages/indexer_missing_tracks_subpage.dart b/lib/ui/pages/subpages/indexer_missing_tracks_subpage.dart index 84d62098..7cfc22be 100644 --- a/lib/ui/pages/subpages/indexer_missing_tracks_subpage.dart +++ b/lib/ui/pages/subpages/indexer_missing_tracks_subpage.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'dart:isolate'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/base/pull_to_refresh.dart'; import 'package:namida/class/track.dart'; @@ -135,7 +135,7 @@ class _IndexerMissingTracksSubpageState extends State e.tracks)) { - tracks.loop((e, _) { + tracks.loop((e) { allTracks[e.track.path] ??= true; }); } @@ -246,9 +246,10 @@ class _IndexerMissingTracksSubpageState extends State Text( - "${_loadingProgress.value.index + 1}/$_loadingCountTotalSteps", + ObxO( + rx: _loadingProgress, + builder: (progress) => Text( + "${progress.index + 1}/$_loadingCountTotalSteps", style: context.textTheme.displayMedium, textAlign: TextAlign.center, ), @@ -256,14 +257,13 @@ class _IndexerMissingTracksSubpageState extends State Text( + "${progress.value}...", + style: context.textTheme.displayMedium, + textAlign: TextAlign.center, + ), ), ], ), @@ -285,7 +285,7 @@ class _IndexerMissingTracksSubpageState extends State extends StatelessWidget { final HistoryManager historyController; final bool Function(MostPlayedTimeRange type) isTimeRangeChipEnabled; final void Function({required MostPlayedTimeRange? mptr, DateRange? dateCustom, bool? isStartOfDay}) onSavingTimeRange; - final List? itemExtents; + final double? itemExtent; final Widget Function(Widget timeRangeChips, double bottomPadding) header; final Widget Function(BuildContext context, int i, RxMap> listensMap) itemBuilder; final Rx customDateRange; @@ -26,7 +26,7 @@ class MostPlayedItemsPage extends StatelessWidget { required this.historyController, required this.isTimeRangeChipEnabled, required this.onSavingTimeRange, - required this.itemExtents, + required this.itemExtent, required this.header, required this.itemBuilder, required this.customDateRange, @@ -80,7 +80,7 @@ class MostPlayedItemsPage extends StatelessWidget { dateText ?? mptr.toText(), style: context.textTheme.displaySmall?.copyWith( color: textColor, - fontSize: dateText == null ? null : 12.0.multipliedFontScale, + fontSize: dateText == null ? null : 12.0, fontWeight: FontWeight.w600, ), ), @@ -143,7 +143,7 @@ class MostPlayedItemsPage extends StatelessWidget { children: [ Obx( () { - final dateRange = customDateRange.value; + final dateRange = customDateRange.valueR; return _getChipChild( context: context, mptr: MostPlayedTimeRange.custom, @@ -186,7 +186,7 @@ class MostPlayedItemsPage extends StatelessWidget { () { final finalListenMap = historyController.currentTopTracksMapListens; return NamidaListView( - itemExtents: itemExtents, + itemExtent: itemExtent, header: header(bottomWidget, bottomPadding), padding: kBottomPaddingInsets, itemCount: finalListenMap.length, diff --git a/lib/ui/pages/subpages/playlist_tracks_subpage.dart b/lib/ui/pages/subpages/playlist_tracks_subpage.dart index 371866f0..3aabf569 100644 --- a/lib/ui/pages/subpages/playlist_tracks_subpage.dart +++ b/lib/ui/pages/subpages/playlist_tracks_subpage.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:known_extents_list_view_builder/sliver_known_extents_list.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:history_manager/history_manager.dart'; +import 'package:namida/base/history_days_rebuilder.dart'; +import 'package:namida/core/utils.dart'; import 'package:sticky_headers/sticky_headers.dart'; import 'package:namida/base/pull_to_refresh.dart'; @@ -34,149 +36,115 @@ class HistoryTracksPage extends StatefulWidget { State createState() => _HistoryTracksPageState(); } -class _HistoryTracksPageState extends State { +class _HistoryTracksPageState extends State with HistoryDaysRebuilderMixin { @override - void initState() { - super.initState(); - HistoryController.inst.canUpdateAllItemsExtentsInHistory = true; - HistoryController.inst.calculateAllItemsExtentsInHistory(); - } - - @override - void dispose() { - HistoryController.inst.canUpdateAllItemsExtentsInHistory = false; - super.dispose(); - } + HistoryManager get historyManager => HistoryController.inst; @override Widget build(BuildContext context) { + final trackTileExtent = Dimensions.inst.trackTileItemExtent; + const dayHeaderExtent = kHistoryDayHeaderHeightWithPadding; + + const dayHeaderHeight = kHistoryDayHeaderHeight; + final dayHeaderBgColor = Color.alphaBlend(context.theme.cardTheme.color!.withAlpha(140), context.theme.scaffoldBackgroundColor); + final dayHeaderSideColor = CurrentColor.inst.color; + final dayHeaderShadowColor = Color.alphaBlend(context.theme.shadowColor.withAlpha(140), context.theme.scaffoldBackgroundColor).withOpacity(0.4); + + final daysLength = historyDays.length; + return BackgroundWrapper( child: CustomScrollView( controller: HistoryController.inst.scrollController, slivers: [ - Obx( - () { - final historyTracks = QueueSource.history.toTracks(); + ObxO( + rx: HistoryController.inst.totalHistoryItemsCount, + builder: (totalHistoryItemsCount) { + final lengthDummy = totalHistoryItemsCount == -1; return SliverToBoxAdapter( child: SubpagesTopContainer( source: QueueSource.history, title: k_PLAYLIST_NAME_HISTORY.translatePlaylistName(), - subtitle: HistoryController.inst.historyTracksLength.displayTrackKeyword, + subtitle: lengthDummy ? '?' : totalHistoryItemsCount.displayTrackKeyword, heroTag: 'playlist_$k_PLAYLIST_NAME_HISTORY', - tracks: historyTracks, - imageWidget: MultiArtworkContainer( - heroTag: 'playlist_$k_PLAYLIST_NAME_HISTORY', - size: Get.width * 0.35, - tracks: historyTracks.toImageTracks(), + tracksFn: () => HistoryController.inst.historyTracks, + imageWidget: ObxO( + rx: HistoryController.inst.historyMap, + builder: (historyMap) => MultiArtworkContainer( + heroTag: 'playlist_$k_PLAYLIST_NAME_HISTORY', + size: context.width * 0.35, + tracks: getHistoryTracks(historyMap).toImageTracks(), + ), ), bottomPadding: 8.0, ), ); }, ), - Obx( - () { - final days = HistoryController.inst.historyDays.toList(); - return SliverKnownExtentsList( - key: UniqueKey(), - itemExtents: HistoryController.inst.allItemsExtentsHistory, - delegate: SliverChildBuilderDelegate( - childCount: HistoryController.inst.historyDays.length, - (context, index) { - final day = days[index]; - final dayInMs = Duration(days: day).inMilliseconds; - final tracks = HistoryController.inst.historyMap.value[day] ?? []; + ObxO( + rx: HistoryController.inst.historyMap, + builder: (history) { + // -- refresh sublist when history change + return SliverVariedExtentList.builder( + key: ValueKey(daysLength), // rebuild after adding/removing day + itemExtentBuilder: (index, dimensions) { + final day = historyDays[index]; + return HistoryController.inst.dayToSectionExtent(day, trackTileExtent, dayHeaderExtent); + }, + itemCount: daysLength, + itemBuilder: (context, index) { + final day = historyDays[index]; + final dayInMs = super.dayToMillis(day); + final tracks = history[day] ?? []; - return StickyHeaderBuilder( - key: ValueKey(index), - builder: (context, stuckAmount) { - final reverseStuck = 1 - stuckAmount; - return Container( - clipBehavior: Clip.antiAlias, - width: context.width, - height: kHistoryDayHeaderHeight, - decoration: BoxDecoration( - color: Color.alphaBlend(context.theme.cardTheme.color!.withAlpha(140), context.theme.scaffoldBackgroundColor), - boxShadow: [ - BoxShadow( - offset: Offset(0, 2.0 * reverseStuck), - blurRadius: 4.0, - color: - Color.alphaBlend(context.theme.shadowColor.withAlpha(140), context.theme.scaffoldBackgroundColor).withOpacity(reverseStuck.clamp(0.0, 0.4)), - ), - ], - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(6.0.multipliedRadius * reverseStuck), - bottomRight: Radius.circular(6.0.multipliedRadius * reverseStuck), - )), - child: Row( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.all(12.0), - decoration: BoxDecoration( - border: Border( - left: BorderSide( - color: CurrentColor.inst.color, - width: (4.0 * stuckAmount).withMinimum(3.0), - ), - ), - ), - child: Text( - [dayInMs.dateFormattedOriginal, tracks.length.displayTrackKeyword].join(' • '), - style: context.textTheme.displayMedium, - ), - ), - ), - NamidaIconButton( - icon: Broken.more, - iconSize: 22.0, - onPressed: () { - showGeneralPopupDialog( - tracks.toTracks(), - dayInMs.dateFormattedOriginal, - tracks.length.displayTrackKeyword, - QueueSource.history, - tracksWithDates: tracks, - playlistName: k_PLAYLIST_NAME_HISTORY, - ); - }, - ), - const SizedBox(width: 2.0), - ], - ), + return StickyHeader( + key: ValueKey(index), + header: NamidaHistoryDayHeaderBox( + height: dayHeaderHeight, + title: [ + dayInMs.dateFormattedOriginal, + tracks.length.displayTrackKeyword, + ].join(' • '), + sideColor: dayHeaderSideColor, + bgColor: dayHeaderBgColor, + shadowColor: dayHeaderShadowColor, + menu: NamidaIconButton( + icon: Broken.more, + horizontalPadding: 8.0, + iconSize: 22.0, + onPressed: () { + showGeneralPopupDialog( + tracks.toTracks(), + dayInMs.dateFormattedOriginal, + tracks.length.displayTrackKeyword, + QueueSource.history, + tracksWithDates: tracks, + playlistName: k_PLAYLIST_NAME_HISTORY, + ); + }, + ), + ), + content: ListView.builder( + padding: const EdgeInsets.only(bottom: kHistoryDayListBottomPadding, top: kHistoryDayListTopPadding), + primary: false, + itemExtent: Dimensions.inst.trackTileItemExtent, + itemCount: tracks.length, + itemBuilder: (context, i) { + final tr = tracks[i]; + return TrackTile( + trackOrTwd: tr, + index: i, + queueSource: QueueSource.history, + bgColor: day == HistoryController.inst.dayOfHighLight.value && i == HistoryController.inst.indexToHighlight.value + ? context.theme.colorScheme.onSurface.withAlpha(40) + : null, + draggableThumbnail: false, + playlistName: k_PLAYLIST_NAME_HISTORY, + thirdLineText: tr.dateAdded.dateAndClockFormattedOriginal, ); }, - content: Obx( - () => SizedBox( - height: HistoryController.inst.allItemsExtentsHistory[index], - width: context.width, - child: ListView.builder( - padding: const EdgeInsets.only(bottom: kHistoryDayListBottomPadding, top: kHistoryDayListTopPadding), - primary: false, - itemExtent: Dimensions.inst.trackTileItemExtent, - itemCount: tracks.length, - itemBuilder: (context, i) { - final tr = tracks[i]; - - return TrackTile( - trackOrTwd: tr, - index: i, - queueSource: QueueSource.history, - bgColor: day == HistoryController.inst.dayOfHighLight.value && i == HistoryController.inst.indexToHighlight.value - ? context.theme.colorScheme.onBackground.withAlpha(40) - : null, - draggableThumbnail: false, - playlistName: k_PLAYLIST_NAME_HISTORY, - thirdLineText: tr.dateAdded.dateAndClockFormattedOriginal, - ); - }, - ), - ), - ), - ); - }, - ), + ), + ); + }, ); }, ), @@ -192,68 +160,70 @@ class MostPlayedTracksPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Obx( - () { - final tracks = QueueSource.mostPlayed.toTracks(); - return MostPlayedItemsPage( - itemExtents: tracks.toTrackItemExtents(), - historyController: HistoryController.inst, - customDateRange: settings.mostPlayedCustomDateRange, - isTimeRangeChipEnabled: (type) => type == settings.mostPlayedTimeRange.value, - onSavingTimeRange: ({dateCustom, isStartOfDay, mptr}) { - settings.save( - mostPlayedTimeRange: mptr, - mostPlayedCustomDateRange: dateCustom, - mostPlayedCustomisStartOfDay: isStartOfDay, - ); - }, - header: (timeRangeChips, bottomPadding) { - return SubpagesTopContainer( - source: QueueSource.mostPlayed, - title: k_PLAYLIST_NAME_MOST_PLAYED.translatePlaylistName(), - subtitle: tracks.displayTrackKeyword, - heroTag: 'playlist_$k_PLAYLIST_NAME_MOST_PLAYED', - imageWidget: MultiArtworkContainer( + return AnimationLimiter( + child: Obx( + () { + final tracks = QueueSource.mostPlayed.toTracks(); + return MostPlayedItemsPage( + itemExtent: Dimensions.inst.trackTileItemExtent, + historyController: HistoryController.inst, + customDateRange: settings.mostPlayedCustomDateRange, + isTimeRangeChipEnabled: (type) => type == settings.mostPlayedTimeRange.value, + onSavingTimeRange: ({dateCustom, isStartOfDay, mptr}) { + settings.save( + mostPlayedTimeRange: mptr, + mostPlayedCustomDateRange: dateCustom, + mostPlayedCustomisStartOfDay: isStartOfDay, + ); + }, + header: (timeRangeChips, bottomPadding) { + return SubpagesTopContainer( + source: QueueSource.mostPlayed, + title: k_PLAYLIST_NAME_MOST_PLAYED.translatePlaylistName(), + subtitle: tracks.displayTrackKeyword, heroTag: 'playlist_$k_PLAYLIST_NAME_MOST_PLAYED', - size: Get.width * 0.35, - tracks: tracks.toImageTracks(), - ), - tracks: tracks, - bottomPadding: bottomPadding, - bottomWidget: timeRangeChips, - ); - }, - itemBuilder: (context, i, listensMap) { - final track = tracks[i]; - final listens = listensMap[track] ?? []; + imageWidget: MultiArtworkContainer( + heroTag: 'playlist_$k_PLAYLIST_NAME_MOST_PLAYED', + size: context.width * 0.35, + tracks: tracks.toImageTracks(), + ), + tracksFn: () => HistoryController.inst.currentMostPlayedTracks, + bottomPadding: bottomPadding, + bottomWidget: timeRangeChips, + ); + }, + itemBuilder: (context, i, listensMap) { + final track = tracks[i]; + final listens = listensMap[track] ?? []; - return AnimatingTile( - key: Key("${track}_$i"), - position: i, - child: TrackTile( + return AnimatingTile( key: Key("${track}_$i"), - draggableThumbnail: false, - index: i, - trackOrTwd: tracks[i], - queueSource: QueueSource.mostPlayed, - playlistName: k_PLAYLIST_NAME_MOST_PLAYED, - onRightAreaTap: () => showTrackListensDialog(track.track, datesOfListen: listens), - trailingWidget: Container( - padding: const EdgeInsets.all(6.0), - decoration: BoxDecoration( - color: context.theme.scaffoldBackgroundColor, - shape: BoxShape.circle, - ), - child: Text( - listens.length.formatDecimal(), - style: context.textTheme.displaySmall, + position: i, + child: TrackTile( + key: Key("${track}_$i"), + draggableThumbnail: false, + index: i, + trackOrTwd: tracks[i], + queueSource: QueueSource.mostPlayed, + playlistName: k_PLAYLIST_NAME_MOST_PLAYED, + onRightAreaTap: () => showTrackListensDialog(track.track, datesOfListen: listens), + trailingWidget: Container( + padding: const EdgeInsets.all(6.0), + decoration: BoxDecoration( + color: context.theme.scaffoldBackgroundColor, + shape: BoxShape.circle, + ), + child: Text( + listens.length.formatDecimal(), + style: context.textTheme.displaySmall, + ), ), ), - ), - ); - }, - ); - }, + ); + }, + ); + }, + ), ); } } @@ -395,9 +365,13 @@ class _NormalPlaylistTracksPageState extends State wit @override Widget build(BuildContext context) { + final threeC = ObxO( + rx: PlaylistController.inst.canReorderTracks, + builder: (reorderable) => ThreeLineSmallContainers(enabled: reorderable), + ); final child = Obx( () { - PlaylistController.inst.playlistsMap.entries; + PlaylistController.inst.playlistsMap.valueR.entries; final playlist = PlaylistController.inst.getPlaylist(widget.playlistName); if (playlist == null) return const SizedBox(); _playlistM3uPath = playlist.m3uPath; @@ -410,7 +384,7 @@ class _NormalPlaylistTracksPageState extends State wit return NamidaListViewRaw( scrollController: _scrollController, itemCount: tracks.length, - itemExtents: tracks.toTrackItemExtents(), + itemExtent: Dimensions.inst.trackTileItemExtent, header: SubpagesTopContainer( source: playlist.toQueueSource(), title: playlist.name.translatePlaylistName(), @@ -419,41 +393,46 @@ class _NormalPlaylistTracksPageState extends State wit heroTag: 'playlist_${playlist.name}', imageWidget: MultiArtworkContainer( heroTag: 'playlist_${playlist.name}', - size: Get.width * 0.35, + size: context.width * 0.35, tracks: tracks.toImageTracks(), ), - tracks: tracks, + tracksFn: () => tracks, ), padding: kBottomPaddingInsets, + onReorderStart: (index) => super.enablePullToRefresh = false, + onReorderEnd: (index) => super.enablePullToRefresh = true, onReorder: (oldIndex, newIndex) => PlaylistController.inst.reorderTrack(playlist, oldIndex, newIndex), itemBuilder: (context, i) { final trackWithDate = tracksWithDate[i]; - final w = Obx( - () { - final reorderable = PlaylistController.inst.canReorderTracks.value; - return FadeDismissible( - key: Key("Diss_$i$trackWithDate"), - direction: reorderable ? DismissDirection.horizontal : DismissDirection.none, - onDismissed: (direction) => NamidaOnTaps.inst.onRemoveTracksFromPlaylist(playlist.name, [trackWithDate]), - child: Stack( - alignment: Alignment.centerLeft, - children: [ - TrackTile( + + return FadeDismissible( + key: Key("Diss_$i$trackWithDate"), + draggableRx: PlaylistController.inst.canReorderTracks, + onDismissed: (direction) => NamidaOnTaps.inst.onRemoveTracksFromPlaylist(playlist.name, [trackWithDate]), + onTopWidget: Positioned( + left: 0, + top: 0, + bottom: 0, + child: threeC, + ), + child: ObxO( + rx: PlaylistController.inst.canReorderTracks, + builder: (reorderable) { + return AnimatingTile( + key: ValueKey(i), + position: i, + shouldAnimate: !(reorderable || widget.disableAnimation), + child: TrackTile( index: i, trackOrTwd: trackWithDate, playlistName: playlist.name, queueSource: playlist.toQueueSource(), draggableThumbnail: reorderable, - selectable: !PlaylistController.inst.canReorderTracks.value, + selectable: () => !PlaylistController.inst.canReorderTracks.value, ), - Obx(() => ThreeLineSmallContainers(enabled: PlaylistController.inst.canReorderTracks.value)), - ], - ), - ); - }, + ); + }), ); - if (widget.disableAnimation) return SizedBox(key: Key(i.toString()), child: w); - return AnimatingTile(key: ValueKey(i), position: i, child: w); }, listBuilder: (list) { return Stack( @@ -466,27 +445,29 @@ class _NormalPlaylistTracksPageState extends State wit ); }, ); - return BackgroundWrapper( - child: _playlistM3uPath != null - ? Listener( - onPointerMove: (event) { - onPointerMove(_scrollController, event); - }, - onPointerUp: (event) async { - final m3uPath = _playlistM3uPath; - if (m3uPath != null) { - onRefresh(() async { - await PlaylistController.inst.prepareM3UPlaylists(forPaths: {m3uPath}); - PlaylistController.inst.sortPlaylists(); - }); - } else { - onVerticalDragFinish(); - } - }, - onPointerCancel: (event) => onVerticalDragFinish(), - child: child, - ) - : child, + return AnimationLimiter( + child: BackgroundWrapper( + child: _playlistM3uPath != null + ? Listener( + onPointerMove: (event) { + onPointerMove(_scrollController, event); + }, + onPointerUp: (event) async { + final m3uPath = _playlistM3uPath; + if (m3uPath != null) { + onRefresh(() async { + await PlaylistController.inst.prepareM3UPlaylists(forPaths: {m3uPath}); + PlaylistController.inst.sortPlaylists(); + }); + } else { + onVerticalDragFinish(); + } + }, + onPointerCancel: (event) => onVerticalDragFinish(), + child: child, + ) + : child, + ), ); } } @@ -494,7 +475,12 @@ class _NormalPlaylistTracksPageState extends State wit class ThreeLineSmallContainers extends StatelessWidget { final bool enabled; final Color? color; - const ThreeLineSmallContainers({Key? key, required this.enabled, this.color}) : super(key: key); + + const ThreeLineSmallContainers({ + super.key, + required this.enabled, + this.color, + }); @override Widget build(BuildContext context) { diff --git a/lib/ui/pages/subpages/queue_tracks_subpage.dart b/lib/ui/pages/subpages/queue_tracks_subpage.dart index 10b38883..81bc551c 100644 --- a/lib/ui/pages/subpages/queue_tracks_subpage.dart +++ b/lib/ui/pages/subpages/queue_tracks_subpage.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/queue.dart'; import 'package:namida/core/enums.dart'; @@ -29,11 +29,11 @@ class QueueTracksPage extends StatelessWidget { ].join(' - '), heroTag: 'queue_${queue.date}', imageWidget: MultiArtworkContainer( - size: Get.width * 0.35, + size: namida.width * 0.35, heroTag: 'queue_${queue.date}', tracks: queue.tracks.toImageTracks(), ), - tracks: queue.tracks, + tracksFn: () => queue.tracks, ), ), ); diff --git a/lib/ui/pages/tracks_page.dart b/lib/ui/pages/tracks_page.dart index 467ba1a3..a661f889 100644 --- a/lib/ui/pages/tracks_page.dart +++ b/lib/ui/pages/tracks_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/base/pull_to_refresh.dart'; import 'package:namida/controller/indexer_controller.dart'; @@ -59,59 +60,61 @@ class _TracksPageState extends State with TickerProviderStateMixin, onRefresh(() async => await showRefreshPromptDialog(false)); }, onPointerCancel: (event) => onVerticalDragFinish(), - child: Obx( - () { - settings.trackListTileHeight.value; - return Column( - children: [ - ExpandableBox( - enableHero: false, - isBarVisible: LibraryTab.tracks.isBarVisible, - showSearchBox: LibraryTab.tracks.isSearchBoxVisible, - displayloadingIndicator: Indexer.inst.isIndexing.value, - leftWidgets: [ - NamidaIconButton( - icon: Broken.shuffle, - onPressed: () => Player.inst.playOrPause(0, SearchSortController.inst.trackSearchList, QueueSource.allTracks, shuffle: true), - iconSize: 18.0, - horizontalPadding: 0, - ), - const SizedBox(width: 12.0), - NamidaIconButton( - icon: Broken.play, - onPressed: () => Player.inst.playOrPause(0, SearchSortController.inst.trackSearchList, QueueSource.allTracks), - iconSize: 18.0, - horizontalPadding: 0, - ), - const SizedBox(width: 12.0), - ], - leftText: SearchSortController.inst.trackSearchList.displayTrackKeyword, - onFilterIconTap: () => ScrollSearchController.inst.switchSearchBoxVisibilty(LibraryTab.tracks), - onCloseButtonPressed: () { - ScrollSearchController.inst.clearSearchTextField(LibraryTab.tracks); - }, - sortByMenuWidget: SortByMenu( - title: settings.tracksSort.value.toText(), - popupMenuChild: () => const SortByMenuTracks(), - isCurrentlyReversed: settings.tracksSortReversed.value, - onReverseIconTap: () { - SearchSortController.inst.sortMedia(MediaType.track, reverse: !settings.tracksSortReversed.value); - }, + child: Column( + children: [ + Obx( + () => ExpandableBox( + enableHero: false, + isBarVisible: LibraryTab.tracks.isBarVisible.valueR, + showSearchBox: LibraryTab.tracks.isSearchBoxVisible.valueR, + displayloadingIndicator: Indexer.inst.isIndexing.valueR, + leftWidgets: [ + NamidaIconButton( + icon: Broken.shuffle, + onPressed: () => Player.inst.playOrPause(0, SearchSortController.inst.trackSearchList.value, QueueSource.allTracks, shuffle: true), + iconSize: 18.0, + horizontalPadding: 0, ), - textField: CustomTextFiled( - textFieldController: LibraryTab.tracks.textSearchController, - textFieldHintText: lang.FILTER_TRACKS, - onTextFieldValueChanged: (value) => SearchSortController.inst.searchMedia(value, MediaType.track), + const SizedBox(width: 12.0), + NamidaIconButton( + icon: Broken.play, + onPressed: () => Player.inst.playOrPause(0, SearchSortController.inst.trackSearchList.value, QueueSource.allTracks), + iconSize: 18.0, + horizontalPadding: 0, ), + const SizedBox(width: 12.0), + ], + leftText: SearchSortController.inst.trackSearchList.valueR.displayTrackKeyword, + onFilterIconTap: () => ScrollSearchController.inst.switchSearchBoxVisibilty(LibraryTab.tracks), + onCloseButtonPressed: () { + ScrollSearchController.inst.clearSearchTextField(LibraryTab.tracks); + }, + sortByMenuWidget: SortByMenu( + title: settings.tracksSort.valueR.toText(), + popupMenuChild: () => const SortByMenuTracks(), + isCurrentlyReversed: settings.tracksSortReversed.valueR, + onReverseIconTap: () { + SearchSortController.inst.sortMedia(MediaType.track, reverse: !settings.tracksSortReversed.value); + }, + ), + textField: () => CustomTextFiled( + textFieldController: LibraryTab.tracks.textSearchController, + textFieldHintText: lang.FILTER_TRACKS, + onTextFieldValueChanged: (value) => SearchSortController.inst.searchMedia(value, MediaType.track), ), - Expanded( - child: NamidaListViewRaw( - itemExtents: List.filled(SearchSortController.inst.trackSearchList.length, Dimensions.inst.trackTileItemExtent), - itemCount: SearchSortController.inst.trackSearchList.length, + ), + ), + Expanded( + child: AnimationLimiter( + child: ObxO( + rx: SearchSortController.inst.trackSearchList, + builder: (trackSearchList) => NamidaListViewRaw( + itemExtent: Dimensions.inst.trackTileItemExtent, + itemCount: trackSearchList.length, scrollController: LibraryTab.tracks.scrollController, scrollStep: Dimensions.inst.trackTileItemExtent, itemBuilder: (context, i) { - final track = SearchSortController.inst.trackSearchList[i]; + final track = trackSearchList[i]; return AnimatingTile( key: Key("$i${track.path}"), position: i, @@ -134,9 +137,9 @@ class _TracksPageState extends State with TickerProviderStateMixin, }, ), ), - ], - ); - }, + ), + ), + ], ), ), ); diff --git a/lib/ui/widgets/artwork.dart b/lib/ui/widgets/artwork.dart index d3e5d988..b107d857 100644 --- a/lib/ui/widgets/artwork.dart +++ b/lib/ui/widgets/artwork.dart @@ -5,8 +5,6 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - import 'package:namida/base/loading_items_delay.dart'; import 'package:namida/class/track.dart'; import 'package:namida/controller/indexer_controller.dart'; @@ -14,6 +12,7 @@ import 'package:namida/controller/settings_controller.dart'; import 'package:namida/core/constants.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/packages/drop_shadow.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; @@ -212,7 +211,7 @@ class _ArtworkWidgetState extends State with LoadingItemsDelayMix int? finalCache; if (widget.compressed || widget.useTrackTileCacheHeight) { - final pixelRatio = context.mediaQuery.devicePixelRatio; + final pixelRatio = context.pixelRatio; final cacheMultiplier = (pixelRatio * settings.artworkCacheHeightMultiplier.value).round(); finalCache = widget.useTrackTileCacheHeight ? 60 * cacheMultiplier : widget.cacheHeight * cacheMultiplier; } @@ -373,166 +372,116 @@ class MultiArtworks extends StatelessWidget { @override Widget build(BuildContext context) { - return NamidaHero( - tag: heroTag, - enabled: !disableHero, - child: Container( - height: thumbnailSize, - width: thumbnailSize, - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration(borderRadius: BorderRadius.circular(borderRadius.multipliedRadius)), - child: LayoutBuilder( - builder: (context, c) { - return tracks.isEmpty - ? ArtworkWidget( - key: const Key(''), - track: null, - thumbnailSize: thumbnailSize, - path: null, - forceSquared: true, - blur: 0, - forceDummyArtwork: true, - bgcolor: bgcolor, - borderRadius: borderRadius, - iconSize: iconSize, - width: c.maxWidth, - height: c.maxHeight, - fallbackToFolderCover: fallbackToFolderCover, - ) - : tracks.length == 1 - ? ArtworkWidget( - key: Key(tracks[0].pathToImage), - thumbnailSize: thumbnailSize, - track: tracks[0], - path: tracks[0].pathToImage, - forceSquared: true, - blur: 0, - borderRadius: 0, - compressed: false, - width: c.maxWidth, - height: c.maxHeight, - fallbackToFolderCover: fallbackToFolderCover, - ) - : tracks.length == 2 - ? Row( - children: [ - ArtworkWidget( - key: Key("0_${tracks[0].pathToImage}"), - thumbnailSize: thumbnailSize / 2, - track: tracks[0], - path: tracks[0].pathToImage, - forceSquared: true, - blur: 0, - borderRadius: 0, - iconSize: iconSize - 2.0, - width: c.maxWidth / 2, - height: c.maxHeight, - fallbackToFolderCover: fallbackToFolderCover, - cacheHeight: reduceQuality ? 60 : 80, - ), - ArtworkWidget( - key: Key("1_${tracks[1].pathToImage}"), - thumbnailSize: thumbnailSize / 2, - track: tracks[1], - path: tracks[1].pathToImage, - forceSquared: true, - blur: 0, - borderRadius: 0, - iconSize: iconSize - 2.0, - width: c.maxWidth / 2, - height: c.maxHeight, - fallbackToFolderCover: fallbackToFolderCover, - cacheHeight: reduceQuality ? 60 : 80, - ), - ], + return SizedBox( + height: thumbnailSize, + width: thumbnailSize, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(borderRadius.multipliedRadius), + ), + child: NamidaHero( + tag: heroTag, + enabled: !disableHero, + child: tracks.isEmpty + ? ArtworkWidget( + key: const Key(''), + track: null, + thumbnailSize: thumbnailSize, + path: null, + forceSquared: true, + blur: 0, + forceDummyArtwork: true, + bgcolor: bgcolor, + borderRadius: borderRadius, + iconSize: iconSize, + width: thumbnailSize, + height: thumbnailSize, + fallbackToFolderCover: fallbackToFolderCover, + ) + : LayoutBuilder( + builder: (context, c) { + return tracks.length == 1 + ? ArtworkWidget( + key: Key(tracks[0].pathToImage), + thumbnailSize: thumbnailSize, + track: tracks[0], + path: tracks[0].pathToImage, + forceSquared: true, + blur: 0, + borderRadius: 0, + compressed: false, + width: c.maxWidth, + height: c.maxHeight, + fallbackToFolderCover: fallbackToFolderCover, ) - : tracks.length == 3 + : tracks.length == 2 ? Row( children: [ - Column( - children: [ - ArtworkWidget( - key: Key("0_${tracks[0].pathToImage}"), - thumbnailSize: thumbnailSize / 2, - track: tracks[0], - path: tracks[0].pathToImage, - forceSquared: true, - blur: 0, - borderRadius: 0, - iconSize: iconSize - 2.0, - width: c.maxWidth / 2, - height: c.maxHeight / 2, - fallbackToFolderCover: fallbackToFolderCover, - cacheHeight: reduceQuality ? 40 : 80, - ), - ArtworkWidget( - key: Key("1_${tracks[1].pathToImage}"), - thumbnailSize: thumbnailSize / 2, - track: tracks[1], - path: tracks[1].pathToImage, - forceSquared: true, - blur: 0, - borderRadius: 0, - iconSize: iconSize - 2.0, - width: c.maxWidth / 2, - height: c.maxHeight / 2, - fallbackToFolderCover: fallbackToFolderCover, - cacheHeight: reduceQuality ? 40 : 80, - ), - ], + ArtworkWidget( + key: Key("0_${tracks[0].pathToImage}"), + thumbnailSize: thumbnailSize / 2, + track: tracks[0], + path: tracks[0].pathToImage, + forceSquared: true, + blur: 0, + borderRadius: 0, + iconSize: iconSize - 2.0, + width: c.maxWidth / 2, + height: c.maxHeight, + fallbackToFolderCover: fallbackToFolderCover, + cacheHeight: reduceQuality ? 60 : 80, ), ArtworkWidget( - key: Key("2_${tracks[2].pathToImage}"), + key: Key("1_${tracks[1].pathToImage}"), thumbnailSize: thumbnailSize / 2, - track: tracks[2], - path: tracks[2].pathToImage, + track: tracks[1], + path: tracks[1].pathToImage, forceSquared: true, blur: 0, borderRadius: 0, - iconSize: iconSize, + iconSize: iconSize - 2.0, width: c.maxWidth / 2, height: c.maxHeight, fallbackToFolderCover: fallbackToFolderCover, - cacheHeight: reduceQuality ? 40 : 80, + cacheHeight: reduceQuality ? 60 : 80, ), ], ) - : Column( - children: [ - Row( + : tracks.length == 3 + ? Row( children: [ - ArtworkWidget( - key: Key("0_${tracks[0].pathToImage}"), - thumbnailSize: thumbnailSize / 2, - track: tracks[0], - path: tracks[0].pathToImage, - forceSquared: true, - blur: 0, - borderRadius: 0, - iconSize: iconSize - 3.0, - width: c.maxWidth / 2, - height: c.maxHeight / 2, - fallbackToFolderCover: fallbackToFolderCover, - cacheHeight: reduceQuality ? 40 : 80, - ), - ArtworkWidget( - key: Key("1_${tracks[1].pathToImage}"), - thumbnailSize: thumbnailSize / 2, - track: tracks[1], - path: tracks[1].pathToImage, - forceSquared: true, - blur: 0, - borderRadius: 0, - iconSize: iconSize - 3.0, - width: c.maxWidth / 2, - height: c.maxHeight / 2, - fallbackToFolderCover: fallbackToFolderCover, - cacheHeight: reduceQuality ? 40 : 80, + Column( + children: [ + ArtworkWidget( + key: Key("0_${tracks[0].pathToImage}"), + thumbnailSize: thumbnailSize / 2, + track: tracks[0], + path: tracks[0].pathToImage, + forceSquared: true, + blur: 0, + borderRadius: 0, + iconSize: iconSize - 2.0, + width: c.maxWidth / 2, + height: c.maxHeight / 2, + fallbackToFolderCover: fallbackToFolderCover, + cacheHeight: reduceQuality ? 40 : 80, + ), + ArtworkWidget( + key: Key("1_${tracks[1].pathToImage}"), + thumbnailSize: thumbnailSize / 2, + track: tracks[1], + path: tracks[1].pathToImage, + forceSquared: true, + blur: 0, + borderRadius: 0, + iconSize: iconSize - 2.0, + width: c.maxWidth / 2, + height: c.maxHeight / 2, + fallbackToFolderCover: fallbackToFolderCover, + cacheHeight: reduceQuality ? 40 : 80, + ), + ], ), - ], - ), - Row( - children: [ ArtworkWidget( key: Key("2_${tracks[2].pathToImage}"), thumbnailSize: thumbnailSize / 2, @@ -541,31 +490,84 @@ class MultiArtworks extends StatelessWidget { forceSquared: true, blur: 0, borderRadius: 0, - iconSize: iconSize - 3.0, + iconSize: iconSize, width: c.maxWidth / 2, - height: c.maxHeight / 2, + height: c.maxHeight, fallbackToFolderCover: fallbackToFolderCover, cacheHeight: reduceQuality ? 40 : 80, ), - ArtworkWidget( - key: Key("3_${tracks[3].pathToImage}"), - thumbnailSize: thumbnailSize / 2, - track: tracks[3], - path: tracks[3].pathToImage, - forceSquared: true, - blur: 0, - borderRadius: 0, - iconSize: iconSize - 3.0, - width: c.maxWidth / 2, - height: c.maxHeight / 2, - fallbackToFolderCover: fallbackToFolderCover, - cacheHeight: reduceQuality ? 40 : 80, + ], + ) + : Column( + children: [ + Row( + children: [ + ArtworkWidget( + key: Key("0_${tracks[0].pathToImage}"), + thumbnailSize: thumbnailSize / 2, + track: tracks[0], + path: tracks[0].pathToImage, + forceSquared: true, + blur: 0, + borderRadius: 0, + iconSize: iconSize - 3.0, + width: c.maxWidth / 2, + height: c.maxHeight / 2, + fallbackToFolderCover: fallbackToFolderCover, + cacheHeight: reduceQuality ? 40 : 80, + ), + ArtworkWidget( + key: Key("1_${tracks[1].pathToImage}"), + thumbnailSize: thumbnailSize / 2, + track: tracks[1], + path: tracks[1].pathToImage, + forceSquared: true, + blur: 0, + borderRadius: 0, + iconSize: iconSize - 3.0, + width: c.maxWidth / 2, + height: c.maxHeight / 2, + fallbackToFolderCover: fallbackToFolderCover, + cacheHeight: reduceQuality ? 40 : 80, + ), + ], + ), + Row( + children: [ + ArtworkWidget( + key: Key("2_${tracks[2].pathToImage}"), + thumbnailSize: thumbnailSize / 2, + track: tracks[2], + path: tracks[2].pathToImage, + forceSquared: true, + blur: 0, + borderRadius: 0, + iconSize: iconSize - 3.0, + width: c.maxWidth / 2, + height: c.maxHeight / 2, + fallbackToFolderCover: fallbackToFolderCover, + cacheHeight: reduceQuality ? 40 : 80, + ), + ArtworkWidget( + key: Key("3_${tracks[3].pathToImage}"), + thumbnailSize: thumbnailSize / 2, + track: tracks[3], + path: tracks[3].pathToImage, + forceSquared: true, + blur: 0, + borderRadius: 0, + iconSize: iconSize - 3.0, + width: c.maxWidth / 2, + height: c.maxHeight / 2, + fallbackToFolderCover: fallbackToFolderCover, + cacheHeight: reduceQuality ? 40 : 80, + ), + ], ), ], - ), - ], - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/ui/widgets/circular_percentages.dart b/lib/ui/widgets/circular_percentages.dart index 623f3fa8..d56c64c3 100644 --- a/lib/ui/widgets/circular_percentages.dart +++ b/lib/ui/widgets/circular_percentages.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - +import 'package:namida/controller/indexer_controller.dart'; import 'package:namida/controller/json_to_history_parser.dart'; import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; -import 'package:namida/controller/indexer_controller.dart'; class IndexingPercentage extends StatelessWidget { final double size; @@ -14,14 +13,14 @@ class IndexingPercentage extends StatelessWidget { @override Widget build(BuildContext context) { - return NamidaHero( - tag: 'indexingper', - child: Obx( - () => NamidaCircularPercentage( - percentage: Indexer.inst.tracksInfoList.length / Indexer.inst.allAudioFiles.length, - size: size, - ).animateEntrance(showWhen: Indexer.inst.isIndexing.value), - ), + return Obx( + () => Indexer.inst.isIndexing.valueR + ? NamidaCircularPercentage( + heroTag: 'indexingper', + percentage: Indexer.inst.tracksInfoList.valueR.length / Indexer.inst.allAudioFiles.valueR.length, + size: size, + ).animateEntrance(showWhen: Indexer.inst.isIndexing.valueR) + : const SizedBox(), ); } } @@ -35,15 +34,13 @@ class ParsingJsonPercentage extends StatelessWidget { @override Widget build(BuildContext context) { return Obx( - () => JsonToHistoryParser.inst.isParsing.value && (forceDisplay ? true : source == JsonToHistoryParser.inst.currentParsingSource.value) + () => JsonToHistoryParser.inst.isParsing.valueR && (forceDisplay ? true : source == JsonToHistoryParser.inst.currentParsingSource.valueR) ? TapDetector( onTap: () => JsonToHistoryParser.inst.showParsingProgressDialog(), - child: NamidaHero( - tag: 'parsingjsonper', - child: NamidaCircularPercentage( - percentage: JsonToHistoryParser.inst.parsedHistoryJson.value / JsonToHistoryParser.inst.totalJsonToParse.value, - size: size, - ), + child: NamidaCircularPercentage( + heroTag: 'parsingjsonper', + percentage: JsonToHistoryParser.inst.parsedHistoryJson.valueR / JsonToHistoryParser.inst.totalJsonToParse.valueR, + size: size, ), ) : const SizedBox(), diff --git a/lib/ui/widgets/custom_reorderable_list.dart b/lib/ui/widgets/custom_reorderable_list.dart new file mode 100644 index 00000000..124f71fe --- /dev/null +++ b/lib/ui/widgets/custom_reorderable_list.dart @@ -0,0 +1,1502 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// An Edited version of flutter [SliverReorderableList] implementation with [setState] removed from some parts +/// to avoid whole list unnecessary rebuilds, improving performance significantly +library; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart' hide ReorderableListView, SliverReorderableList, ReorderableDragStartListener; +import 'package:flutter/rendering.dart'; + +// Examples can assume: +// class MyDataObject {} + +/// A callback used by [ReorderableList] to report that a list item has moved +/// to a new position in the list. +/// +/// Implementations should remove the corresponding list item at [oldIndex] +/// and reinsert it at [newIndex]. +/// +/// If [oldIndex] is before [newIndex], removing the item at [oldIndex] from the +/// list will reduce the list's length by one. Implementations will need to +/// account for this when inserting before [newIndex]. +/// +/// {@youtube 560 315 https://www.youtube.com/watch?v=3fB1mxOsqJE} +/// +/// {@tool snippet} +/// +/// ```dart +/// final List backingList = [/* ... */]; +/// +/// void handleReorder(int oldIndex, int newIndex) { +/// if (oldIndex < newIndex) { +/// // removing the item at oldIndex will shorten the list by 1. +/// newIndex -= 1; +/// } +/// final MyDataObject element = backingList.removeAt(oldIndex); +/// backingList.insert(newIndex, element); +/// } +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [ReorderableList], a widget list that allows the user to reorder +/// its items. +/// * [SliverReorderableList], a sliver list that allows the user to reorder +/// its items. +/// * [ReorderableListView], a Material Design list that allows the user to +/// reorder its items. +typedef ReorderCallback = void Function(int oldIndex, int newIndex); + +/// Signature for the builder callback used to decorate the dragging item in +/// [ReorderableList] and [SliverReorderableList]. +/// +/// The [child] will be the item that is being dragged, and [index] is the +/// position of the item in the list. +/// +/// The [animation] will be driven forward from 0.0 to 1.0 while the item is +/// being picked up during a drag operation, and reversed from 1.0 to 0.0 when +/// the item is dropped. This can be used to animate properties of the proxy +/// like an elevation or border. +/// +/// The returned value will typically be the [child] wrapped in other widgets. +typedef ReorderItemProxyDecorator = Widget Function(Widget child, int index, Animation animation); + +/// A scrolling container that allows the user to interactively reorder the +/// list items. +/// +/// This widget is similar to one created by [ListView.builder], and uses +/// an [IndexedWidgetBuilder] to create each item. +/// +/// It is up to the application to wrap each child (or an internal part of the +/// child such as a drag handle) with a drag listener that will recognize +/// the start of an item drag and then start the reorder by calling +/// [ReorderableListState.startItemDragReorder]. This is most easily achieved +/// by wrapping each child in a [ReorderableDragStartListener] or a +/// [ReorderableDelayedDragStartListener]. These will take care of recognizing +/// the start of a drag gesture and call the list state's +/// [ReorderableListState.startItemDragReorder] method. +/// +/// This widget's [ReorderableListState] can be used to manually start an item +/// reorder, or cancel a current drag. To refer to the +/// [ReorderableListState] either provide a [GlobalKey] or use the static +/// [ReorderableList.of] method from an item's build method. +/// +/// See also: +/// +/// * [SliverReorderableList], a sliver list that allows the user to reorder +/// its items. +/// * [ReorderableListView], a Material Design list that allows the user to +/// reorder its items. +class ReorderableList extends StatefulWidget { + /// Creates a scrolling container that allows the user to interactively + /// reorder the list items. + /// + /// The [itemCount] must be greater than or equal to zero. + const ReorderableList({ + super.key, + required this.itemBuilder, + required this.itemCount, + required this.onReorder, + this.onReorderStart, + this.onReorderEnd, + this.itemExtent, + this.itemExtentBuilder, + this.prototypeItem, + this.proxyDecorator, + this.padding, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.controller, + this.primary, + this.physics, + this.shrinkWrap = false, + this.anchor = 0.0, + this.cacheExtent, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + this.autoScrollerVelocityScalar, + }) : assert(itemCount >= 0), + assert( + (itemExtent == null && prototypeItem == null) || (itemExtent == null && itemExtentBuilder == null) || (prototypeItem == null && itemExtentBuilder == null), + 'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.', + ); + + /// {@template flutter.widgets.reorderable_list.itemBuilder} + /// Called, as needed, to build list item widgets. + /// + /// List items are only built when they're scrolled into view. + /// + /// The [IndexedWidgetBuilder] index parameter indicates the item's + /// position in the list. The value of the index parameter will be between + /// zero and one less than [itemCount]. All items in the list must have a + /// unique [Key], and should have some kind of listener to start the drag + /// (usually a [ReorderableDragStartListener] or + /// [ReorderableDelayedDragStartListener]). + /// {@endtemplate} + final IndexedWidgetBuilder itemBuilder; + + /// {@template flutter.widgets.reorderable_list.itemCount} + /// The number of items in the list. + /// + /// It must be a non-negative integer. When zero, nothing is displayed and + /// the widget occupies no space. + /// {@endtemplate} + final int itemCount; + + /// {@template flutter.widgets.reorderable_list.onReorder} + /// A callback used by the list to report that a list item has been dragged + /// to a new location in the list and the application should update the order + /// of the items. + /// {@endtemplate} + final ReorderCallback onReorder; + + /// {@template flutter.widgets.reorderable_list.onReorderStart} + /// A callback that is called when an item drag has started. + /// + /// The index parameter of the callback is the index of the selected item. + /// + /// See also: + /// + /// * [onReorderEnd], which is a called when the dragged item is dropped. + /// * [onReorder], which reports that a list item has been dragged to a new + /// location. + /// {@endtemplate} + final void Function(int index)? onReorderStart; + + /// {@template flutter.widgets.reorderable_list.onReorderEnd} + /// A callback that is called when the dragged item is dropped. + /// + /// The index parameter of the callback is the index where the item is + /// dropped. Unlike [onReorder], this is called even when the list item is + /// dropped in the same location. + /// + /// See also: + /// + /// * [onReorderStart], which is a called when an item drag has started. + /// * [onReorder], which reports that a list item has been dragged to a new + /// location. + /// {@endtemplate} + final void Function(int index)? onReorderEnd; + + /// {@template flutter.widgets.reorderable_list.proxyDecorator} + /// A callback that allows the app to add an animated decoration around + /// an item when it is being dragged. + /// {@endtemplate} + final ReorderItemProxyDecorator? proxyDecorator; + + /// {@template flutter.widgets.reorderable_list.padding} + /// The amount of space by which to inset the list contents. + /// + /// It defaults to `EdgeInsets.all(0)`. + /// {@endtemplate} + final EdgeInsetsGeometry? padding; + + /// {@macro flutter.widgets.scroll_view.scrollDirection} + final Axis scrollDirection; + + /// {@macro flutter.widgets.scroll_view.reverse} + final bool reverse; + + /// {@macro flutter.widgets.scroll_view.controller} + final ScrollController? controller; + + /// {@macro flutter.widgets.scroll_view.primary} + final bool? primary; + + /// {@macro flutter.widgets.scroll_view.physics} + final ScrollPhysics? physics; + + /// {@macro flutter.widgets.scroll_view.shrinkWrap} + final bool shrinkWrap; + + /// {@macro flutter.widgets.scroll_view.anchor} + final double anchor; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior} + /// + /// The default is [ScrollViewKeyboardDismissBehavior.manual] + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// {@macro flutter.widgets.scrollable.restorationId} + final String? restorationId; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + /// {@macro flutter.widgets.list_view.itemExtent} + final double? itemExtent; + + /// {@macro flutter.widgets.list_view.itemExtentBuilder} + final ItemExtentBuilder? itemExtentBuilder; + + /// {@macro flutter.widgets.list_view.prototypeItem} + final Widget? prototypeItem; + + /// {@macro flutter.widgets.EdgeDraggingAutoScroller.velocityScalar} + /// + /// {@macro flutter.widgets.SliverReorderableList.autoScrollerVelocityScalar.default} + final double? autoScrollerVelocityScalar; + + /// The state from the closest instance of this class that encloses the given + /// context. + /// + /// This method is typically used by [ReorderableList] item widgets that + /// insert or remove items in response to user input. + /// + /// If no [ReorderableList] surrounds the given context, then this function + /// will assert in debug mode and throw an exception in release mode. + /// + /// This method can be expensive (it walks the element tree). + /// + /// See also: + /// + /// * [maybeOf], a similar function that will return null if no + /// [ReorderableList] ancestor is found. + static ReorderableListState of(BuildContext context) { + final ReorderableListState? result = context.findAncestorStateOfType(); + assert(() { + if (result == null) { + throw FlutterError.fromParts([ + ErrorSummary('ReorderableList.of() called with a context that does not contain a ReorderableList.'), + ErrorDescription( + 'No ReorderableList ancestor could be found starting from the context that was passed to ReorderableList.of().', + ), + ErrorHint( + 'This can happen when the context provided is from the same StatefulWidget that ' + 'built the ReorderableList. Please see the ReorderableList documentation for examples ' + 'of how to refer to an ReorderableListState object:\n' + ' https://api.flutter.dev/flutter/widgets/ReorderableListState-class.html', + ), + context.describeElement('The context used was'), + ]); + } + return true; + }()); + return result!; + } + + /// The state from the closest instance of this class that encloses the given + /// context. + /// + /// This method is typically used by [ReorderableList] item widgets that insert + /// or remove items in response to user input. + /// + /// If no [ReorderableList] surrounds the context given, then this function will + /// return null. + /// + /// This method can be expensive (it walks the element tree). + /// + /// See also: + /// + /// * [of], a similar function that will throw if no [ReorderableList] ancestor + /// is found. + static ReorderableListState? maybeOf(BuildContext context) { + return context.findAncestorStateOfType(); + } + + @override + ReorderableListState createState() => ReorderableListState(); +} + +/// The state for a list that allows the user to interactively reorder +/// the list items. +/// +/// An app that needs to start a new item drag or cancel an existing one +/// can refer to the [ReorderableList]'s state with a global key: +/// +/// ```dart +/// GlobalKey listKey = GlobalKey(); +/// // ... +/// Widget build(BuildContext context) { +/// return ReorderableList( +/// key: listKey, +/// itemBuilder: (BuildContext context, int index) => const SizedBox(height: 10.0), +/// itemCount: 5, +/// onReorder: (int oldIndex, int newIndex) { +/// // ... +/// }, +/// ); +/// } +/// // ... +/// listKey.currentState!.cancelReorder(); +/// ``` +class ReorderableListState extends State { + final GlobalKey _sliverReorderableListKey = GlobalKey(); + + /// Initiate the dragging of the item at [index] that was started with + /// the pointer down [event]. + /// + /// The given [recognizer] will be used to recognize and start the drag + /// item tracking and lead to either an item reorder, or a canceled drag. + /// The list will take ownership of the returned recognizer and will dispose + /// it when it is no longer needed. + /// + /// Most applications will not use this directly, but will wrap the item + /// (or part of the item, like a drag handle) in either a + /// [ReorderableDragStartListener] or [ReorderableDelayedDragStartListener] + /// which call this for the application. + void startItemDragReorder({ + required int index, + required PointerDownEvent event, + required MultiDragGestureRecognizer recognizer, + }) { + _sliverReorderableListKey.currentState!.startItemDragReorder(index: index, event: event, recognizer: recognizer); + } + + /// Cancel any item drag in progress. + /// + /// This should be called before any major changes to the item list + /// occur so that any item drags will not get confused by + /// changes to the underlying list. + /// + /// If no drag is active, this will do nothing. + void cancelReorder() { + _sliverReorderableListKey.currentState!.cancelReorder(); + } + + @override + Widget build(BuildContext context) { + return CustomScrollView( + scrollDirection: widget.scrollDirection, + reverse: widget.reverse, + controller: widget.controller, + primary: widget.primary, + physics: widget.physics, + shrinkWrap: widget.shrinkWrap, + anchor: widget.anchor, + cacheExtent: widget.cacheExtent, + dragStartBehavior: widget.dragStartBehavior, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + restorationId: widget.restorationId, + clipBehavior: widget.clipBehavior, + slivers: [ + SliverPadding( + padding: widget.padding ?? EdgeInsets.zero, + sliver: SliverReorderableList( + key: _sliverReorderableListKey, + itemExtent: widget.itemExtent, + prototypeItem: widget.prototypeItem, + itemBuilder: widget.itemBuilder, + itemCount: widget.itemCount, + onReorder: widget.onReorder, + onReorderStart: widget.onReorderStart, + onReorderEnd: widget.onReorderEnd, + proxyDecorator: widget.proxyDecorator, + autoScrollerVelocityScalar: widget.autoScrollerVelocityScalar, + ), + ), + ], + ); + } +} + +/// A sliver list that allows the user to interactively reorder the list items. +/// +/// It is up to the application to wrap each child (or an internal part of the +/// child) with a drag listener that will recognize the start of an item drag +/// and then start the reorder by calling +/// [SliverReorderableListState.startItemDragReorder]. This is most easily +/// achieved by wrapping each child in a [ReorderableDragStartListener] or +/// a [ReorderableDelayedDragStartListener]. These will take care of +/// recognizing the start of a drag gesture and call the list state's start +/// item drag method. +/// +/// This widget's [SliverReorderableListState] can be used to manually start an item +/// reorder, or cancel a current drag that's already underway. To refer to the +/// [SliverReorderableListState] either provide a [GlobalKey] or use the static +/// [SliverReorderableList.of] method from an item's build method. +/// +/// See also: +/// +/// * [ReorderableList], a regular widget list that allows the user to reorder +/// its items. +/// * [ReorderableListView], a Material Design list that allows the user to +/// reorder its items. +class SliverReorderableList extends StatefulWidget { + /// Creates a sliver list that allows the user to interactively reorder its + /// items. + /// + /// The [itemCount] must be greater than or equal to zero. + const SliverReorderableList({ + super.key, + required this.itemBuilder, + this.findChildIndexCallback, + required this.itemCount, + required this.onReorder, + this.onReorderStart, + this.onReorderEnd, + this.itemExtent, + this.itemExtentBuilder, + this.prototypeItem, + this.proxyDecorator, + double? autoScrollerVelocityScalar, + }) : autoScrollerVelocityScalar = autoScrollerVelocityScalar ?? _kDefaultAutoScrollVelocityScalar, + assert(itemCount >= 0), + assert( + (itemExtent == null && prototypeItem == null) || (itemExtent == null && itemExtentBuilder == null) || (prototypeItem == null && itemExtentBuilder == null), + 'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.', + ); + + // An eyeballed value for a smooth scrolling experience. + static const double _kDefaultAutoScrollVelocityScalar = 50; + + /// {@macro flutter.widgets.reorderable_list.itemBuilder} + final IndexedWidgetBuilder itemBuilder; + + /// {@macro flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback} + final ChildIndexGetter? findChildIndexCallback; + + /// {@macro flutter.widgets.reorderable_list.itemCount} + final int itemCount; + + /// {@macro flutter.widgets.reorderable_list.onReorder} + final ReorderCallback onReorder; + + /// {@macro flutter.widgets.reorderable_list.onReorderStart} + final void Function(int)? onReorderStart; + + /// {@macro flutter.widgets.reorderable_list.onReorderEnd} + final void Function(int)? onReorderEnd; + + /// {@macro flutter.widgets.reorderable_list.proxyDecorator} + final ReorderItemProxyDecorator? proxyDecorator; + + /// {@macro flutter.widgets.list_view.itemExtent} + final double? itemExtent; + + /// {@macro flutter.widgets.list_view.itemExtentBuilder} + final ItemExtentBuilder? itemExtentBuilder; + + /// {@macro flutter.widgets.list_view.prototypeItem} + final Widget? prototypeItem; + + /// {@macro flutter.widgets.EdgeDraggingAutoScroller.velocityScalar} + /// + /// {@template flutter.widgets.SliverReorderableList.autoScrollerVelocityScalar.default} + /// Defaults to 50 if not set or set to null. + /// {@endtemplate} + final double autoScrollerVelocityScalar; + + @override + SliverReorderableListState createState() => SliverReorderableListState(); + + /// The state from the closest instance of this class that encloses the given + /// context. + /// + /// This method is typically used by [SliverReorderableList] item widgets to + /// start or cancel an item drag operation. + /// + /// If no [SliverReorderableList] surrounds the context given, this function + /// will assert in debug mode and throw an exception in release mode. + /// + /// This method can be expensive (it walks the element tree). + /// + /// See also: + /// + /// * [maybeOf], a similar function that will return null if no + /// [SliverReorderableList] ancestor is found. + static SliverReorderableListState of(BuildContext context) { + final SliverReorderableListState? result = context.findAncestorStateOfType(); + assert(() { + if (result == null) { + throw FlutterError.fromParts([ + ErrorSummary( + 'SliverReorderableList.of() called with a context that does not contain a SliverReorderableList.', + ), + ErrorDescription( + 'No SliverReorderableList ancestor could be found starting from the context that was passed to SliverReorderableList.of().', + ), + ErrorHint( + 'This can happen when the context provided is from the same StatefulWidget that ' + 'built the SliverReorderableList. Please see the SliverReorderableList documentation for examples ' + 'of how to refer to an SliverReorderableList object:\n' + ' https://api.flutter.dev/flutter/widgets/SliverReorderableListState-class.html', + ), + context.describeElement('The context used was'), + ]); + } + return true; + }()); + return result!; + } + + /// The state from the closest instance of this class that encloses the given + /// context. + /// + /// This method is typically used by [SliverReorderableList] item widgets that + /// insert or remove items in response to user input. + /// + /// If no [SliverReorderableList] surrounds the context given, this function + /// will return null. + /// + /// This method can be expensive (it walks the element tree). + /// + /// See also: + /// + /// * [of], a similar function that will throw if no [SliverReorderableList] + /// ancestor is found. + static SliverReorderableListState? maybeOf(BuildContext context) { + return context.findAncestorStateOfType(); + } +} + +/// The state for a sliver list that allows the user to interactively reorder +/// the list items. +/// +/// An app that needs to start a new item drag or cancel an existing one +/// can refer to the [SliverReorderableList]'s state with a global key: +/// +/// ```dart +/// // (e.g. in a stateful widget) +/// GlobalKey listKey = GlobalKey(); +/// +/// // ... +/// +/// @override +/// Widget build(BuildContext context) { +/// return SliverReorderableList( +/// key: listKey, +/// itemBuilder: (BuildContext context, int index) => const SizedBox(height: 10.0), +/// itemCount: 5, +/// onReorder: (int oldIndex, int newIndex) { +/// // ... +/// }, +/// ); +/// } +/// +/// // ... +/// +/// void _stop() { +/// listKey.currentState!.cancelReorder(); +/// } +/// ``` +/// +/// [ReorderableDragStartListener] and [ReorderableDelayedDragStartListener] +/// refer to their [SliverReorderableList] with the static +/// [SliverReorderableList.of] method. +class SliverReorderableListState extends State with TickerProviderStateMixin { + // Map of index -> child state used manage where the dragging item will need + // to be inserted. + final Map _items = {}; + + OverlayEntry? _overlayEntry; + int? _dragIndex; + _DragInfo? _dragInfo; + int? _insertIndex; + int? _initialDragIndex; + Offset? _finalDropPosition; + MultiDragGestureRecognizer? _recognizer; + int? _recognizerPointer; + + EdgeDraggingAutoScroller? _autoScroller; + + late ScrollableState _scrollable; + Axis get _scrollDirection => axisDirectionToAxis(_scrollable.axisDirection); + bool get _reverse => _scrollable.axisDirection == AxisDirection.up || _scrollable.axisDirection == AxisDirection.left; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _scrollable = Scrollable.of(context); + if (_autoScroller?.scrollable != _scrollable) { + _autoScroller?.stopAutoScroll(); + _autoScroller = EdgeDraggingAutoScroller( + _scrollable, + onScrollViewScrolled: _handleScrollableAutoScrolled, + velocityScalar: widget.autoScrollerVelocityScalar, + ); + } + } + + @override + void didUpdateWidget(covariant SliverReorderableList oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.itemCount != oldWidget.itemCount) { + cancelReorder(); + } + + if (widget.autoScrollerVelocityScalar != oldWidget.autoScrollerVelocityScalar) { + _autoScroller?.stopAutoScroll(); + _autoScroller = EdgeDraggingAutoScroller( + _scrollable, + onScrollViewScrolled: _handleScrollableAutoScrolled, + velocityScalar: widget.autoScrollerVelocityScalar, + ); + } + } + + @override + void dispose() { + _dragReset(); + _recognizer?.dispose(); + super.dispose(); + } + + /// Initiate the dragging of the item at [index] that was started with + /// the pointer down [event]. + /// + /// The given [recognizer] will be used to recognize and start the drag + /// item tracking and lead to either an item reorder, or a canceled drag. + /// + /// Most applications will not use this directly, but will wrap the item + /// (or part of the item, like a drag handle) in either a + /// [ReorderableDragStartListener] or [ReorderableDelayedDragStartListener] + /// which call this method when they detect the gesture that triggers a drag + /// start. + void startItemDragReorder({ + required int index, + required PointerDownEvent event, + required MultiDragGestureRecognizer recognizer, + }) { + assert(0 <= index && index < widget.itemCount); + if (_dragInfo != null) { + cancelReorder(); + } else if (_recognizer != null && _recognizerPointer != event.pointer) { + _recognizer!.dispose(); + _recognizer = null; + _recognizerPointer = null; + } + + if (_items.containsKey(index)) { + _dragIndex = index; + _recognizer = recognizer + ..onStart = _dragStart + ..addPointer(event); + _recognizerPointer = event.pointer; + } else { + // TODO(darrenaustin): Can we handle this better, maybe scroll to the item? + throw Exception('Attempting to start a drag on a non-visible item'); + } + } + + /// Cancel any item drag in progress. + /// + /// This should be called before any major changes to the item list + /// occur so that any item drags will not get confused by + /// changes to the underlying list. + /// + /// If a drag operation is in progress, this will immediately reset + /// the list to back to its pre-drag state. + /// + /// If no drag is active, this will do nothing. + void cancelReorder() { + _dragReset(); + } + + void _registerItem(_ReorderableItemState item) { + if (_dragInfo != null && _items[item.index] != item) { + item.updateForGap(_dragInfo!.index, _dragInfo!.index, _dragInfo!.itemExtent, false, _reverse); + } + _items[item.index] = item; + if (item.index == _dragInfo?.index) { + item.dragging = true; + item.rebuild(); + } + } + + void _unregisterItem(int index, _ReorderableItemState item) { + final _ReorderableItemState? currentItem = _items[index]; + if (currentItem == item) { + _items.remove(index); + } + } + + Drag? _dragStart(Offset position) { + assert(_dragInfo == null); + final _ReorderableItemState item = _items[_dragIndex!]!; + item.dragging = true; + widget.onReorderStart?.call(_dragIndex!); + item.rebuild(); + + _insertIndex = item.index; + _initialDragIndex = item.index; + _dragInfo = _DragInfo( + item: item, + initialPosition: position, + scrollDirection: _scrollDirection, + onUpdate: _dragUpdate, + onCancel: _dragCancel, + onEnd: _dragEnd, + onDropCompleted: _dropCompleted, + proxyDecorator: widget.proxyDecorator, + tickerProvider: this, + ); + _dragInfo!.startDrag(); + + final OverlayState overlay = Overlay.of(context, debugRequiredFor: widget); + assert(_overlayEntry == null); + _overlayEntry = OverlayEntry(builder: _dragInfo!.createProxy); + overlay.insert(_overlayEntry!); + + for (final _ReorderableItemState childItem in _items.values) { + if (childItem == item || !childItem.mounted) { + continue; + } + childItem.updateForGap(_insertIndex!, _insertIndex!, _dragInfo!.itemExtent, false, _reverse); + } + return _dragInfo; + } + + void _dragUpdate(_DragInfo item, Offset position, Offset delta) { + _overlayEntry?.markNeedsBuild(); + _dragUpdateItems(); + _autoScroller?.startAutoScrollIfNecessary(_dragTargetRect); + } + + void _dragCancel(_DragInfo item) { + _dragReset(); + } + + void _dragEnd(_DragInfo item) { + if (_insertIndex == item.index) { + _finalDropPosition = _itemOffsetAt(_insertIndex!); + } else if (_reverse) { + if (_insertIndex! >= _items.length) { + // Drop at the starting position of the last element and offset its own extent + _finalDropPosition = _itemOffsetAt(_items.length - 1) - _extentOffset(item.itemExtent, _scrollDirection); + } else { + // Drop at the end of the current element occupying the insert position + _finalDropPosition = _itemOffsetAt(_insertIndex!) + _extentOffset(_itemExtentAt(_insertIndex!), _scrollDirection); + } + } else { + if (_insertIndex! == 0) { + // Drop at the starting position of the first element and offset its own extent + _finalDropPosition = _itemOffsetAt(0) - _extentOffset(item.itemExtent, _scrollDirection); + } else { + // Drop at the end of the previous element occupying the insert position + final int atIndex = _insertIndex! - 1; + _finalDropPosition = _itemOffsetAt(atIndex) + _extentOffset(_itemExtentAt(atIndex), _scrollDirection); + } + } + widget.onReorderEnd?.call(_insertIndex!); + } + + void _dropCompleted() { + final int fromIndex = _dragIndex!; + final int toIndex = _insertIndex!; + if (fromIndex != toIndex) { + widget.onReorder.call(fromIndex, toIndex); + } + _dragReset(); + } + + void _dragReset() { + if (_dragInfo != null) { + if (_dragIndex != null && _items.containsKey(_dragIndex)) { + final _ReorderableItemState dragItem = _items[_dragIndex!]!; + dragItem.dragging = false; + dragItem.rebuild(); + _dragIndex = null; + } + _dragInfo?.dispose(); + _dragInfo = null; + _autoScroller?.stopAutoScroll(); + _resetItemGap(); + _recognizer?.dispose(); + _recognizer = null; + _overlayEntry?.remove(); + _overlayEntry?.dispose(); + _overlayEntry = null; + _finalDropPosition = null; + } + } + + void _resetItemGap() { + for (final _ReorderableItemState item in _items.values) { + item.resetGap(); + } + } + + void _handleScrollableAutoScrolled() { + if (_dragInfo == null) { + return; + } + _dragUpdateItems(); + // Continue scrolling if the drag is still in progress. + _autoScroller?.startAutoScrollIfNecessary(_dragTargetRect); + } + + void _dragUpdateItems() { + assert(_dragInfo != null); + final double gapExtent = _dragInfo!.itemExtent; + final double proxyItemStart = _offsetExtent(_dragInfo!.dragPosition - _dragInfo!.dragOffset, _scrollDirection); + final double proxyItemEnd = proxyItemStart + gapExtent; + + // Find the new index for inserting the item being dragged. + int newIndex = _insertIndex!; + for (final _ReorderableItemState item in _items.values) { + if (item.index == _dragIndex! || !item.mounted) { + continue; + } + + final Rect geometry = item.targetGeometry(); + final double itemStart = _scrollDirection == Axis.vertical ? geometry.top : geometry.left; + final double itemExtent = _scrollDirection == Axis.vertical ? geometry.height : geometry.width; + final double itemEnd = itemStart + itemExtent; + final double itemMiddle = itemStart + itemExtent / 2; + + if (_reverse) { + if (itemEnd >= proxyItemEnd && proxyItemEnd >= itemMiddle) { + // The start of the proxy is in the beginning half of the item, so + // we should swap the item with the gap and we are done looking for + // the new index. + newIndex = item.index; + break; + } else if (itemMiddle >= proxyItemStart && proxyItemStart >= itemStart) { + // The end of the proxy is in the ending half of the item, so + // we should swap the item with the gap and we are done looking for + // the new index. + newIndex = item.index + 1; + break; + } else if (itemStart > proxyItemEnd && newIndex < (item.index + 1)) { + newIndex = item.index + 1; + } else if (proxyItemStart > itemEnd && newIndex > item.index) { + newIndex = item.index; + } + } else { + if (itemStart <= proxyItemStart && proxyItemStart <= itemMiddle) { + // The start of the proxy is in the beginning half of the item, so + // we should swap the item with the gap and we are done looking for + // the new index. + if (_initialDragIndex == newIndex - 2) { + newIndex = item.index - 1; // workaround/fix for when dragging item downwards, then to its original index again. + } else { + newIndex = item.index; + } + break; + } else if (itemMiddle <= proxyItemEnd && proxyItemEnd <= itemEnd) { + // The end of the proxy is in the ending half of the item, so + // we should swap the item with the gap and we are done looking for + // the new index. + newIndex = item.index + 1; + break; + } else if (itemEnd < proxyItemStart && newIndex < (item.index + 1)) { + newIndex = item.index + 1; + } else if (proxyItemEnd < itemStart && newIndex > item.index) { + newIndex = item.index; + } + } + } + + if (newIndex != _insertIndex) { + _insertIndex = newIndex; + for (final _ReorderableItemState item in _items.values) { + if (item.index == _dragIndex! || !item.mounted) { + continue; + } + item.updateForGap(_dragIndex!, newIndex, gapExtent, true, _reverse); + } + } + } + + Rect get _dragTargetRect { + final Offset origin = _dragInfo!.dragPosition - _dragInfo!.dragOffset; + return Rect.fromLTWH(origin.dx, origin.dy, _dragInfo!.itemSize.width, _dragInfo!.itemSize.height); + } + + Offset _itemOffsetAt(int index) { + return _items[index]!.targetGeometry().topLeft; + } + + double _itemExtentAt(int index) { + return _sizeExtent(_items[index]!.targetGeometry().size, _scrollDirection); + } + + Widget _itemBuilder(BuildContext context, int index) { + if (_dragInfo != null && index >= widget.itemCount) { + return switch (_scrollDirection) { + Axis.horizontal => SizedBox(width: _dragInfo!.itemExtent), + Axis.vertical => SizedBox(height: _dragInfo!.itemExtent), + }; + } + final Widget child = widget.itemBuilder(context, index); + assert(child.key != null, 'All list items must have a key'); + final OverlayState overlay = Overlay.of(context, debugRequiredFor: widget); + return _ReorderableItem( + key: _ReorderableItemGlobalKey(child.key!, index, this), + index: index, + capturedThemes: InheritedTheme.capture(from: context, to: overlay.context), + child: _wrapWithSemantics(child, index), + ); + } + + Widget _wrapWithSemantics(Widget child, int index) { + void reorder(int startIndex, int endIndex) { + if (startIndex != endIndex) { + widget.onReorder(startIndex, endIndex); + } + } + + // First, determine which semantics actions apply. + final Map semanticsActions = {}; + + // Create the appropriate semantics actions. + void moveToStart() => reorder(index, 0); + void moveToEnd() => reorder(index, widget.itemCount); + void moveBefore() => reorder(index, index - 1); + // To move after, go to index+2 because it is moved to the space + // before index+2, which is after the space at index+1. + void moveAfter() => reorder(index, index + 2); + + final WidgetsLocalizations localizations = WidgetsLocalizations.of(context); + final bool isHorizontal = _scrollDirection == Axis.horizontal; + // If the item can move to before its current position in the list. + if (index > 0) { + semanticsActions[CustomSemanticsAction(label: localizations.reorderItemToStart)] = moveToStart; + String reorderItemBefore = localizations.reorderItemUp; + if (isHorizontal) { + reorderItemBefore = Directionality.of(context) == TextDirection.ltr ? localizations.reorderItemLeft : localizations.reorderItemRight; + } + semanticsActions[CustomSemanticsAction(label: reorderItemBefore)] = moveBefore; + } + + // If the item can move to after its current position in the list. + if (index < widget.itemCount - 1) { + String reorderItemAfter = localizations.reorderItemDown; + if (isHorizontal) { + reorderItemAfter = Directionality.of(context) == TextDirection.ltr ? localizations.reorderItemRight : localizations.reorderItemLeft; + } + semanticsActions[CustomSemanticsAction(label: reorderItemAfter)] = moveAfter; + semanticsActions[CustomSemanticsAction(label: localizations.reorderItemToEnd)] = moveToEnd; + } + + // Pass toWrap with a GlobalKey into the item so that when it + // gets dragged, the accessibility framework can preserve the selected + // state of the dragging item. + // + // Also apply the relevant custom accessibility actions for moving the item + // up, down, to the start, and to the end of the list. + return Semantics( + container: true, + customSemanticsActions: semanticsActions, + child: child, + ); + } + + @override + Widget build(BuildContext context) { + assert(debugCheckHasOverlay(context)); + final SliverChildBuilderDelegate childrenDelegate = SliverChildBuilderDelegate( + _itemBuilder, + childCount: widget.itemCount, + findChildIndexCallback: widget.findChildIndexCallback, + ); + if (widget.itemExtent != null) { + return SliverFixedExtentList( + delegate: childrenDelegate, + itemExtent: widget.itemExtent!, + ); + } else if (widget.itemExtentBuilder != null) { + return SliverVariedExtentList( + delegate: childrenDelegate, + itemExtentBuilder: widget.itemExtentBuilder!, + ); + } else if (widget.prototypeItem != null) { + return SliverPrototypeExtentList( + delegate: childrenDelegate, + prototypeItem: widget.prototypeItem!, + ); + } + return SliverList(delegate: childrenDelegate); + } +} + +class _ReorderableItem extends StatefulWidget { + const _ReorderableItem({ + required Key key, + required this.index, + required this.child, + required this.capturedThemes, + }) : super(key: key); + + final int index; + final Widget child; + final CapturedThemes capturedThemes; + + @override + _ReorderableItemState createState() => _ReorderableItemState(); +} + +class _ReorderableItemState extends State<_ReorderableItem> { + late SliverReorderableListState _listState; + + Offset _startOffset = Offset.zero; + Offset _targetOffset = Offset.zero; + AnimationController? _offsetAnimation; + + Key get key => widget.key!; + int get index => widget.index; + + bool dragging = false; + + @override + void initState() { + _listState = SliverReorderableList.of(context); + _listState._registerItem(this); + super.initState(); + } + + @override + void dispose() { + _offsetAnimation?.dispose(); + _listState._unregisterItem(index, this); + super.dispose(); + } + + @override + void didUpdateWidget(covariant _ReorderableItem oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.index != widget.index) { + _listState._unregisterItem(oldWidget.index, this); + _listState._registerItem(this); + } + } + + @override + Widget build(BuildContext context) { + if (dragging) { + final Size size = _extentSize(_listState._dragInfo!.itemExtent, _listState._scrollDirection); + return SizedBox.fromSize(size: size); + } + _listState._registerItem(this); + return Transform( + transform: Matrix4.translationValues(offset.dx, offset.dy, 0.0), + child: widget.child, + ); + } + + @override + void deactivate() { + _listState._unregisterItem(index, this); + super.deactivate(); + } + + Offset get offset { + if (_offsetAnimation != null) { + final double animValue = Curves.easeInOut.transform(_offsetAnimation!.value); + return Offset.lerp(_startOffset, _targetOffset, animValue)!; + } + return _targetOffset; + } + + void updateForGap(int dragIndex, int gapIndex, double gapExtent, bool animate, bool reverse) { + // An offset needs to be added to create a gap when we are between the + // moving element (dragIndex) and the current gap position (gapIndex). + // For how to update the gap position, refer to [_dragUpdateItems]. + final Offset newTargetOffset; + if (gapIndex < dragIndex && index < dragIndex && index >= gapIndex) { + newTargetOffset = _extentOffset(reverse ? -gapExtent : gapExtent, _listState._scrollDirection); + } else if (gapIndex > dragIndex && index > dragIndex && index < gapIndex) { + newTargetOffset = _extentOffset(reverse ? gapExtent : -gapExtent, _listState._scrollDirection); + } else { + newTargetOffset = Offset.zero; + } + if (newTargetOffset != _targetOffset) { + _targetOffset = newTargetOffset; + if (animate) { + if (_offsetAnimation == null) { + _offsetAnimation = AnimationController( + vsync: _listState, + duration: const Duration(milliseconds: 250), + ) + ..addListener(rebuild) + ..addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) { + _startOffset = _targetOffset; + _offsetAnimation!.dispose(); + _offsetAnimation = null; + } + }) + ..forward(); + } else { + _startOffset = offset; + _offsetAnimation!.forward(from: 0.0); + } + } else { + if (_offsetAnimation != null) { + _offsetAnimation!.dispose(); + _offsetAnimation = null; + } + _startOffset = _targetOffset; + } + rebuild(); + } + } + + void resetGap() { + if (_offsetAnimation != null) { + _offsetAnimation!.dispose(); + _offsetAnimation = null; + } + _startOffset = Offset.zero; + _targetOffset = Offset.zero; + rebuild(); + } + + Rect targetGeometry() { + final RenderBox itemRenderBox = context.findRenderObject()! as RenderBox; + final Offset itemPosition = itemRenderBox.localToGlobal(Offset.zero) + _targetOffset; + return itemPosition & itemRenderBox.size; + } + + void rebuild() { + if (mounted) { + setState(() {}); + } + } +} + +/// A wrapper widget that will recognize the start of a drag on the wrapped +/// widget by a [PointerDownEvent], and immediately initiate dragging the +/// wrapped item to a new location in a reorderable list. +/// +/// See also: +/// +/// * [ReorderableDelayedDragStartListener], a similar wrapper that will +/// only recognize the start after a long press event. +/// * [ReorderableList], a widget list that allows the user to reorder +/// its items. +/// * [SliverReorderableList], a sliver list that allows the user to reorder +/// its items. +/// * [ReorderableListView], a Material Design list that allows the user to +/// reorder its items. +class ReorderableDragStartListener extends StatelessWidget { + /// Creates a listener for a drag immediately following a pointer down + /// event over the given child widget. + /// + /// This is most commonly used to wrap part of a list item like a drag + /// handle. + const ReorderableDragStartListener({ + super.key, + required this.child, + required this.index, + this.enabled = true, + }); + + /// The widget for which the application would like to respond to a tap and + /// drag gesture by starting a reordering drag on a reorderable list. + final Widget child; + + /// The index of the associated item that will be dragged in the list. + final int index; + + /// Whether the [child] item can be dragged and moved in the list. + /// + /// If true, the item can be moved to another location in the list when the + /// user taps on the child. If false, tapping on the child will be ignored. + final bool enabled; + + @override + Widget build(BuildContext context) { + return Listener( + onPointerDown: enabled ? (PointerDownEvent event) => _startDragging(context, event) : null, + child: child, + ); + } + + /// Provides the gesture recognizer used to indicate the start of a reordering + /// drag operation. + /// + /// By default this returns an [ImmediateMultiDragGestureRecognizer] but + /// subclasses can use this to customize the drag start gesture. + @protected + MultiDragGestureRecognizer createRecognizer() { + return ImmediateMultiDragGestureRecognizer(debugOwner: this); + } + + void _startDragging(BuildContext context, PointerDownEvent event) { + final DeviceGestureSettings? gestureSettings = MediaQuery.maybeGestureSettingsOf(context); + final SliverReorderableListState? list = SliverReorderableList.maybeOf(context); + list?.startItemDragReorder( + index: index, + event: event, + recognizer: createRecognizer()..gestureSettings = gestureSettings, + ); + } +} + +/// A wrapper widget that will recognize the start of a drag operation by +/// looking for a long press event. Once it is recognized, it will start +/// a drag operation on the wrapped item in the reorderable list. +/// +/// See also: +/// +/// * [ReorderableDragStartListener], a similar wrapper that will +/// recognize the start of the drag immediately after a pointer down event. +/// * [ReorderableList], a widget list that allows the user to reorder +/// its items. +/// * [SliverReorderableList], a sliver list that allows the user to reorder +/// its items. +/// * [ReorderableListView], a Material Design list that allows the user to +/// reorder its items. +class ReorderableDelayedDragStartListener extends ReorderableDragStartListener { + final Duration delay; + + /// Creates a listener for an drag following a long press event over the + /// given child widget. + /// + /// This is most commonly used to wrap an entire list item in a reorderable + /// list. + const ReorderableDelayedDragStartListener({ + this.delay = const Duration(milliseconds: 20), + super.key, + required super.child, + required super.index, + }); + + @override + MultiDragGestureRecognizer createRecognizer() { + return DelayedMultiDragGestureRecognizer(delay: delay, debugOwner: this); + } +} + +typedef _DragItemUpdate = void Function(_DragInfo item, Offset position, Offset delta); +typedef _DragItemCallback = void Function(_DragInfo item); + +class _DragInfo extends Drag { + _DragInfo({ + required _ReorderableItemState item, + Offset initialPosition = Offset.zero, + this.scrollDirection = Axis.vertical, + this.onUpdate, + this.onEnd, + this.onCancel, + this.onDropCompleted, + this.proxyDecorator, + required this.tickerProvider, + }) { + // TODO(polina-c): stop duplicating code across disposables + // https://github.com/flutter/flutter/issues/137435 + if (kFlutterMemoryAllocationsEnabled) { + FlutterMemoryAllocations.instance.dispatchObjectCreated( + library: 'package:flutter/widgets.dart', + className: '$_DragInfo', + object: this, + ); + } + final RenderBox itemRenderBox = item.context.findRenderObject()! as RenderBox; + listState = item._listState; + index = item.index; + child = item.widget.child; + capturedThemes = item.widget.capturedThemes; + dragPosition = initialPosition; + dragOffset = itemRenderBox.globalToLocal(initialPosition); + itemSize = item.context.size!; + itemExtent = _sizeExtent(itemSize, scrollDirection); + scrollable = Scrollable.of(item.context); + } + + final Axis scrollDirection; + final _DragItemUpdate? onUpdate; + final _DragItemCallback? onEnd; + final _DragItemCallback? onCancel; + final VoidCallback? onDropCompleted; + final ReorderItemProxyDecorator? proxyDecorator; + final TickerProvider tickerProvider; + + late SliverReorderableListState listState; + late int index; + late Widget child; + late Offset dragPosition; + late Offset dragOffset; + late Size itemSize; + late double itemExtent; + late CapturedThemes capturedThemes; + ScrollableState? scrollable; + AnimationController? _proxyAnimation; + + void dispose() { + if (kFlutterMemoryAllocationsEnabled) { + FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this); + } + _proxyAnimation?.dispose(); + } + + void startDrag() { + _proxyAnimation = AnimationController( + vsync: tickerProvider, + duration: const Duration(milliseconds: 250), + ) + ..addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.dismissed) { + _dropCompleted(); + } + }) + ..forward(); + } + + @override + void update(DragUpdateDetails details) { + final Offset delta = _restrictAxis(details.delta, scrollDirection); + dragPosition += delta; + onUpdate?.call(this, dragPosition, details.delta); + } + + @override + void end(DragEndDetails details) { + _proxyAnimation!.reverse(); + onEnd?.call(this); + } + + @override + void cancel() { + _proxyAnimation?.dispose(); + _proxyAnimation = null; + onCancel?.call(this); + } + + void _dropCompleted() { + _proxyAnimation?.dispose(); + _proxyAnimation = null; + onDropCompleted?.call(); + } + + Widget createProxy(BuildContext context) { + return capturedThemes.wrap( + _DragItemProxy( + listState: listState, + index: index, + size: itemSize, + animation: _proxyAnimation!, + position: dragPosition - dragOffset - _overlayOrigin(context), + proxyDecorator: proxyDecorator, + child: child, + ), + ); + } +} + +Offset _overlayOrigin(BuildContext context) { + final OverlayState overlay = Overlay.of(context, debugRequiredFor: context.widget); + final RenderBox overlayBox = overlay.context.findRenderObject()! as RenderBox; + return overlayBox.localToGlobal(Offset.zero); +} + +class _DragItemProxy extends StatelessWidget { + const _DragItemProxy({ + required this.listState, + required this.index, + required this.child, + required this.position, + required this.size, + required this.animation, + required this.proxyDecorator, + }); + + final SliverReorderableListState listState; + final int index; + final Widget child; + final Offset position; + final Size size; + final AnimationController animation; + final ReorderItemProxyDecorator? proxyDecorator; + + @override + Widget build(BuildContext context) { + final Widget proxyChild = proxyDecorator?.call(child, index, animation.view) ?? child; + final Offset overlayOrigin = _overlayOrigin(context); + + return MediaQuery( + // Remove the top padding so that any nested list views in the item + // won't pick up the scaffold's padding in the overlay. + data: MediaQuery.of(context).removePadding(removeTop: true), + child: AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget? child) { + Offset effectivePosition = position; + final Offset? dropPosition = listState._finalDropPosition; + if (dropPosition != null) { + effectivePosition = Offset.lerp(dropPosition - overlayOrigin, effectivePosition, Curves.easeOut.transform(animation.value))!; + } + return Positioned( + left: effectivePosition.dx, + top: effectivePosition.dy, + child: SizedBox( + width: size.width, + height: size.height, + child: child, + ), + ); + }, + child: proxyChild, + ), + ); + } +} + +double _sizeExtent(Size size, Axis scrollDirection) { + return switch (scrollDirection) { + Axis.horizontal => size.width, + Axis.vertical => size.height, + }; +} + +Size _extentSize(double extent, Axis scrollDirection) { + switch (scrollDirection) { + case Axis.horizontal: + return Size(extent, 0); + case Axis.vertical: + return Size(0, extent); + } +} + +double _offsetExtent(Offset offset, Axis scrollDirection) { + return switch (scrollDirection) { + Axis.horizontal => offset.dx, + Axis.vertical => offset.dy, + }; +} + +Offset _extentOffset(double extent, Axis scrollDirection) { + return switch (scrollDirection) { + Axis.horizontal => Offset(extent, 0.0), + Axis.vertical => Offset(0.0, extent), + }; +} + +Offset _restrictAxis(Offset offset, Axis scrollDirection) { + return switch (scrollDirection) { + Axis.horizontal => Offset(offset.dx, 0.0), + Axis.vertical => Offset(0.0, offset.dy), + }; +} + +// A global key that takes its identity from the object and uses a value of a +// particular type to identify itself. +// +// The difference with GlobalObjectKey is that it uses [==] instead of [identical] +// of the objects used to generate widgets. +@optionalTypeArgs +class _ReorderableItemGlobalKey extends GlobalObjectKey { + const _ReorderableItemGlobalKey(this.subKey, this.index, this.state) : super(subKey); + + final Key subKey; + final int index; + final SliverReorderableListState state; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is _ReorderableItemGlobalKey && other.subKey == subKey && other.index == index && other.state == state; + } + + @override + int get hashCode => Object.hash(subKey, index, state); +} diff --git a/lib/ui/widgets/custom_widgets.dart b/lib/ui/widgets/custom_widgets.dart index 44105eb7..42010840 100644 --- a/lib/ui/widgets/custom_widgets.dart +++ b/lib/ui/widgets/custom_widgets.dart @@ -3,14 +3,11 @@ import 'dart:ui'; import 'package:calendar_date_picker2/calendar_date_picker2.dart'; import 'package:checkmark/checkmark.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart' hide ReorderableDragStartListener; +import 'package:flutter/material.dart' hide ReorderableListView, SliverReorderableList, ReorderableDragStartListener, ReorderableDelayedDragStartListener; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_scrollbar_modified/flutter_scrollbar_modified.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; -import 'package:get/get.dart'; import 'package:history_manager/history_manager.dart'; -import 'package:known_extents_list_view_builder/known_extents_reorderable_list_view_builder.dart'; -import 'package:known_extents_list_view_builder/known_extents_sliver_reorderable_list.dart'; import 'package:like_button/like_button.dart'; import 'package:selectable_autolink_text/selectable_autolink_text.dart'; import 'package:sleek_circular_slider/sleek_circular_slider.dart'; @@ -18,7 +15,6 @@ import 'package:wheel_slider/wheel_slider.dart'; import 'package:namida/class/track.dart'; import 'package:namida/controller/current_color.dart'; -import 'package:namida/controller/miniplayer_controller.dart'; import 'package:namida/controller/navigator_controller.dart'; import 'package:namida/controller/player_controller.dart'; import 'package:namida/controller/playlist_controller.dart'; @@ -32,6 +28,7 @@ import 'package:namida/core/functions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/packages/scroll_physics_modified.dart'; import 'package:namida/ui/dialogs/setting_dialog_with_text_field.dart'; import 'package:namida/ui/pages/about_page.dart'; @@ -40,40 +37,15 @@ import 'package:namida/ui/widgets/animated_widgets.dart'; import 'package:namida/ui/widgets/library/track_tile.dart'; import 'package:namida/ui/widgets/settings/extra_settings.dart'; -class CustomReorderableDelayedDragStartListener extends ReorderableDragStartListener { - final Duration delay; - - const CustomReorderableDelayedDragStartListener({ - this.delay = const Duration(milliseconds: 20), - Key? key, - required Widget child, - required int index, - PointerDownEventListener? onDragStart, - PointerUpEventListener? onDragEnd, - }) : super( - key: key, - child: child, - index: index, - onDragStart: onDragStart, - onDragEnd: onDragEnd, - ); - - @override - MultiDragGestureRecognizer createRecognizer() { - return DelayedMultiDragGestureRecognizer(delay: delay, debugOwner: this); - } -} +import 'custom_reorderable_list.dart'; class NamidaReordererableListener extends StatelessWidget { - /// Will disable miniplayer listener when dragging. - final bool isInQueue; final int index; final int durationMs; final Widget child; const NamidaReordererableListener({ super.key, - required this.isInQueue, required this.index, this.durationMs = 50, required this.child, @@ -81,15 +53,7 @@ class NamidaReordererableListener extends StatelessWidget { @override Widget build(BuildContext context) { - return CustomReorderableDelayedDragStartListener( - onDragStart: (event) { - if (isInQueue) { - MiniPlayerController.inst.invokeStartReordering(); - } - }, - onDragEnd: (event) { - MiniPlayerController.inst.invokeDoneReordering(); - }, + return ReorderableDelayedDragStartListener( index: index, delay: Duration(milliseconds: durationMs), child: child, @@ -129,7 +93,7 @@ class CustomSwitch extends StatelessWidget { duration: Duration(milliseconds: durationInMillisecond), decoration: BoxDecoration( color: (active - ? bgColor ?? Color.alphaBlend(finalColor.withAlpha(180), context.theme.colorScheme.background).withAlpha(140) + ? bgColor ?? Color.alphaBlend(finalColor.withAlpha(180), context.theme.colorScheme.surface).withAlpha(140) // : context.theme.scaffoldBackgroundColor.withAlpha(34) : Color.alphaBlend(context.theme.scaffoldBackgroundColor.withAlpha(60), context.theme.disabledColor)), borderRadius: BorderRadius.circular(30.0.multipliedRadius), @@ -138,7 +102,7 @@ class CustomSwitch extends StatelessWidget { offset: const Offset(0, 2), blurRadius: active ? 8 : 2, spreadRadius: 0, - color: (shadowColor ?? Color.alphaBlend(finalColor.withAlpha(180), context.theme.colorScheme.background)).withOpacity(active ? 0.8 : 0.3), + color: (shadowColor ?? Color.alphaBlend(finalColor.withAlpha(180), context.theme.colorScheme.surface)).withOpacity(active ? 0.8 : 0.3), ), ], ), @@ -183,7 +147,7 @@ class CustomSwitchListTile extends StatelessWidget { final Color? bgColor; const CustomSwitchListTile({ - Key? key, + super.key, required this.value, required this.onChanged, required this.title, @@ -197,7 +161,7 @@ class CustomSwitchListTile extends StatelessWidget { this.maxSubtitleLines = 8, this.visualDensity, this.bgColor, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -250,7 +214,7 @@ class CustomListTile extends StatelessWidget { final Color? bgColor; const CustomListTile({ - Key? key, + super.key, required this.title, this.subtitle, this.trailing, @@ -268,7 +232,7 @@ class CustomListTile extends StatelessWidget { this.titleStyle, this.borderR = 20.0, this.bgColor, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -326,7 +290,7 @@ class CustomListTile extends StatelessWidget { child: trailingText != null ? Text( trailingText!, - style: context.textTheme.displayMedium?.copyWith(color: context.theme.colorScheme.onBackground.withAlpha(200)), + style: context.textTheme.displayMedium?.copyWith(color: context.theme.colorScheme.onSurface.withAlpha(200)), ) : trailing, ), @@ -522,6 +486,16 @@ class CustomBlurryDialog extends StatelessWidget { } } +class NamidaButtonText extends Text { + const NamidaButtonText( + super.data, { + super.key, + TextStyle? style, + super.softWrap, + super.overflow, + }) : super(style: style ?? const TextStyle(fontSize: 15.0)); +} + class NamidaButton extends StatelessWidget { final IconData? icon; final double? iconSize; @@ -579,7 +553,7 @@ class NamidaButton extends StatelessWidget { } final iconChild = Icon(icon, size: iconSize); - final textChild = Text( + final textChild = NamidaButtonText( text ?? '', softWrap: false, overflow: TextOverflow.ellipsis, @@ -780,6 +754,7 @@ class SmallListTile extends StatelessWidget { class ListTileWithCheckMark extends StatelessWidget { final bool active; + final RxBase? activeRx; final void Function()? onTap; final String? title; final String subtitle; @@ -792,7 +767,8 @@ class ListTileWithCheckMark extends StatelessWidget { const ListTileWithCheckMark({ super.key, - required this.active, + this.active = false, + this.activeRx, this.onTap, this.title, this.subtitle = '', @@ -809,9 +785,10 @@ class ListTileWithCheckMark extends StatelessWidget { final tileAlpha = context.isDarkMode ? 5 : 20; return Material( borderRadius: BorderRadius.circular(14.0.multipliedRadius), - color: tileColor ?? Color.alphaBlend(context.theme.colorScheme.onBackground.withAlpha(tileAlpha), context.theme.cardTheme.color!), + color: tileColor ?? Color.alphaBlend(context.theme.colorScheme.onSurface.withAlpha(tileAlpha), context.theme.cardTheme.color!), child: ListTile( horizontalTitleGap: dense ? 10.0 : 14.0, + contentPadding: const EdgeInsets.symmetric(horizontal: 12.0), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0.multipliedRadius)), leading: leading ?? Icon( @@ -829,10 +806,18 @@ class ListTileWithCheckMark extends StatelessWidget { style: context.textTheme.displaySmall, ) : null, - trailing: NamidaCheckMark( - size: 18.0, - active: active, - ), + trailing: activeRx != null + ? ObxO( + rx: activeRx!, + builder: (active) => NamidaCheckMark( + size: 18.0, + active: active, + ), + ) + : NamidaCheckMark( + size: 18.0, + active: active, + ), // visualDensity: VisualDensity.compact, visualDensity: const VisualDensity(horizontal: -2.8, vertical: -2.8), onTap: onTap, @@ -1146,7 +1131,7 @@ class CancelButton extends StatelessWidget { Widget build(BuildContext context) { return TextButton( onPressed: onPressed ?? () => NamidaNavigator.inst.closeDialog(), - child: Text(lang.CANCEL), + child: NamidaButtonText(lang.CANCEL), ); } } @@ -1157,12 +1142,14 @@ class CollapsedSettingTileWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Obx( - () => CustomSwitchListTile( + Localizations.localeOf(context); + return ObxO( + rx: settings.useSettingCollapsedTiles, + builder: (useSettingCollapsedTiles) => CustomSwitchListTile( bgColor: bgColor, icon: Broken.archive, title: lang.USE_COLLAPSED_SETTING_TILES, - value: settings.useSettingCollapsedTiles.value, + value: useSettingCollapsedTiles, onChanged: (isTrue) async { settings.save(useSettingCollapsedTiles: !isTrue); await NamidaNavigator.inst.popPage(); @@ -1178,17 +1165,13 @@ class AboutPageTileWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Obx( - () { - settings.selectedLanguage.value; - return CustomCollapsedListTile( - title: lang.ABOUT, - subtitle: null, - icon: Broken.info_circle, - rawPage: true, - page: const AboutPage(), - ); - }, + Localizations.localeOf(context); + return CustomCollapsedListTile( + title: lang.ABOUT, + subtitle: null, + icon: Broken.info_circle, + rawPage: true, + page: const AboutPage(), ); } } @@ -1324,7 +1307,7 @@ class NamidaWheelSlider extends StatelessWidget { itemSize: itemSize, squeeze: squeeze, isInfinite: isInfinite, - lineColor: Get.iconColor, + lineColor: context.theme.iconTheme.color, pointerColor: context.theme.listTileTheme.textColor!, pointerHeight: 38.0, horizontalListHeight: 38.0, @@ -1409,7 +1392,7 @@ class NamidaLikeButton extends StatelessWidget { size: size, enabledColor: color, disabledColor: color, - isLiked: track?.isFavourite, + isLiked: track?.isFavouriteR, onTap: (isLiked) async { if (track != null) { PlaylistController.inst.favouriteButtonOnPressed(track!); @@ -1525,9 +1508,10 @@ class NamidaPartyContainer extends StatelessWidget { @override Widget build(BuildContext context) { if (!settings.enablePartyModeColorSwap.value) { - return Obx( - () { - final finalScale = WaveformController.inst.getCurrentAnimatingScale(Player.inst.nowPlayingPosition); + return ObxO( + rx: Player.inst.nowPlayingPosition, + builder: (nowPlayingPosition) { + final finalScale = WaveformController.inst.getCurrentAnimatingScale(nowPlayingPosition); return AnimatedSizedBox( duration: const Duration(milliseconds: 400), height: height ?? context.height, @@ -1545,51 +1529,56 @@ class NamidaPartyContainer extends StatelessWidget { }, ); } else { - return Obx( - () { - final finalScale = WaveformController.inst.getCurrentAnimatingScale(Player.inst.nowPlayingPosition); - final firstHalf = CurrentColor.inst.paletteFirstHalf; - final secondHalf = CurrentColor.inst.paletteSecondHalf; + return ObxO( + rx: Player.inst.nowPlayingPosition, + builder: (nowPlayingPosition) { + final finalScale = WaveformController.inst.getCurrentAnimatingScale(nowPlayingPosition); return height != null - ? Row( - children: [ - ...firstHalf.map( - (e) => AnimatedSizedBox( - duration: const Duration(milliseconds: 400), - height: height, - width: width ?? context.width / firstHalf.length, - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: e.withAlpha(150), - spreadRadius: 150 * finalScale * spreadRadiusMultiplier, - blurRadius: 10 + (200 * finalScale), - ), - ], + ? ObxO( + rx: CurrentColor.inst.paletteFirstHalf, + builder: (firstHalf) => Row( + children: [ + ...firstHalf.map( + (e) => AnimatedSizedBox( + duration: const Duration(milliseconds: 400), + height: height, + width: width ?? context.width / firstHalf.length, + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: e.withAlpha(150), + spreadRadius: 150 * finalScale * spreadRadiusMultiplier, + blurRadius: 10 + (200 * finalScale), + ), + ], + ), ), ), - ), - ], + ], + ), ) - : Column( - children: [ - ...secondHalf.map( - (e) => AnimatedSizedBox( - duration: const Duration(milliseconds: 400), - height: height ?? context.height / secondHalf.length, - width: width, - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: e.withAlpha(150), - spreadRadius: 140 * finalScale * spreadRadiusMultiplier, - blurRadius: 10 + (200 * finalScale), - ), - ], + : ObxO( + rx: CurrentColor.inst.paletteSecondHalf, + builder: (secondHalf) => Column( + children: [ + ...secondHalf.map( + (e) => AnimatedSizedBox( + duration: const Duration(milliseconds: 400), + height: height ?? context.height / secondHalf.length, + width: width, + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: e.withAlpha(150), + spreadRadius: 140 * finalScale * spreadRadiusMultiplier, + blurRadius: 10 + (200 * finalScale), + ), + ], + ), ), ), - ), - ], + ], + ), ); }, ); @@ -1605,7 +1594,7 @@ class SubpagesTopContainer extends StatelessWidget { final double topPadding; final double bottomPadding; final Widget imageWidget; - final List tracks; + final Iterable Function() tracksFn; final QueueSource source; final String heroTag; final Widget? bottomWidget; @@ -1616,7 +1605,7 @@ class SubpagesTopContainer extends StatelessWidget { this.thirdLineText = '', this.height, required this.imageWidget, - required this.tracks, + required this.tracksFn, this.topPadding = 16.0, this.bottomPadding = 16.0, required this.source, @@ -1674,7 +1663,7 @@ class SubpagesTopContainer extends StatelessWidget { subtitle, overflow: TextOverflow.ellipsis, maxLines: 1, - style: context.textTheme.displayMedium?.copyWith(fontSize: 14.0.multipliedFontScale), + style: context.textTheme.displayMedium?.copyWith(fontSize: 14.0), ), ), ), @@ -1690,7 +1679,7 @@ class SubpagesTopContainer extends StatelessWidget { thirdLineText, overflow: TextOverflow.ellipsis, maxLines: 1, - style: context.textTheme.displaySmall?.copyWith(fontSize: 14.0.multipliedFontScale), + style: context.textTheme.displaySmall?.copyWith(fontSize: 14.0), ), ), ), @@ -1709,7 +1698,7 @@ class SubpagesTopContainer extends StatelessWidget { icon: Broken.shuffle, onPressed: () => Player.inst.playOrPause( 0, - tracks, + tracksFn(), source, shuffle: true, ), @@ -1718,7 +1707,7 @@ class SubpagesTopContainer extends StatelessWidget { const SizedBox(width: 6.0), Expanded( child: ElevatedButton.icon( - onPressed: () => Player.inst.addToQueue(tracks), + onPressed: () => Player.inst.addToQueue(tracksFn()), icon: const StackedIcon( disableColor: true, baseIcon: Broken.play, @@ -1728,7 +1717,7 @@ class SubpagesTopContainer extends StatelessWidget { lang.PLAY_LAST, softWrap: false, overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: (constraints.maxWidth * 0.1).clamp(10.0, 14.0).multipliedFontScale), + style: TextStyle(fontSize: (constraints.maxWidth * 0.1).clamp(10.0, 14.0)), ), style: ElevatedButton.styleFrom( fixedSize: const Size(0.0, 0.0), @@ -1758,18 +1747,27 @@ class AnimatingTile extends StatelessWidget { final int position; final Widget child; final bool shouldAnimate; - const AnimatingTile({super.key, required this.position, required this.child, this.shouldAnimate = true}); + final Duration duration; + + const AnimatingTile({ + super.key, + required this.position, + required this.child, + this.duration = const Duration(milliseconds: 400), + this.shouldAnimate = true, + }); @override Widget build(BuildContext context) { return shouldAnimate ? AnimationConfiguration.staggeredList( position: position, - duration: const Duration(milliseconds: 400), + duration: duration, + delay: const Duration(milliseconds: 50), child: SlideAnimation( verticalOffset: 25.0, child: FadeInAnimation( - duration: const Duration(milliseconds: 400), + duration: duration, child: child, ), ), @@ -1783,7 +1781,14 @@ class AnimatingGrid extends StatelessWidget { final int columnCount; final Widget child; final bool shouldAnimate; - const AnimatingGrid({super.key, required this.position, required this.columnCount, required this.child, this.shouldAnimate = true}); + + const AnimatingGrid({ + super.key, + required this.position, + required this.columnCount, + required this.child, + this.shouldAnimate = true, + }); @override Widget build(BuildContext context) { @@ -1868,7 +1873,7 @@ class NamidaDrawerListTile extends StatelessWidget { title, style: context.textTheme.displayMedium?.copyWith( color: enabled ? Colors.white.withAlpha(200) : null, - fontSize: 15.0.multipliedFontScale, + fontSize: 15.0, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -1917,7 +1922,7 @@ class SearchPageTitleRow extends StatelessWidget { children: [ Text( title, - style: context.textTheme.displayLarge?.copyWith(fontSize: 15.5.multipliedFontScale), + style: context.textTheme.displayLarge?.copyWith(fontSize: 15.5), ), if (subtitleWidget != null) subtitleWidget!, if (subtitle != '') @@ -1934,7 +1939,7 @@ class SearchPageTitleRow extends StatelessWidget { TextButton.icon( style: TextButton.styleFrom(foregroundColor: context.theme.listTileTheme.iconColor), icon: Icon(buttonIcon, size: 20.0), - label: Text(buttonText ?? ''), + label: NamidaButtonText(buttonText ?? ''), onPressed: onPressed, ), const SizedBox(width: 8.0), @@ -1989,7 +1994,7 @@ class NamidaLogoContainer extends StatelessWidget { 'Namida', style: context.textTheme.displayLarge?.copyWith( color: Color.alphaBlend(const Color(0xffdfc6a7).withAlpha(90), Colors.white), - fontSize: 17.5.multipliedFontScale, + fontSize: 17.5, ), ), ], @@ -2023,7 +2028,7 @@ class NamidaContainerDivider extends StatelessWidget { width: width, margin: margin, decoration: BoxDecoration( - color: colorForce ?? (color ?? context.theme.dividerColor).withAlpha(Get.isDarkMode ? 100 : 20), + color: colorForce ?? (color ?? context.theme.dividerColor).withAlpha(namida.isDarkMode ? 100 : 20), borderRadius: BorderRadius.circular(18.0.multipliedRadius), ), ); @@ -2032,45 +2037,164 @@ class NamidaContainerDivider extends StatelessWidget { class FadeDismissible extends StatefulWidget { final Widget child; - final void Function(DismissDirection onDismissed)? onDismissed; - final void Function(DismissUpdateDetails detailts)? onUpdate; + final void Function(DismissDirection onDismissed) onDismissed; + final void Function(DragStartDetails details)? onDismissStart; + final void Function(DragEndDetails details)? onDismissEnd; final DismissDirection direction; + final Duration dismissDuration; + final Duration settleDuration; + final double dismissThreshold; + final double dismissRangeStart; + final double dismissRangeEnd; + final Curve dismissCurve; + final Curve settleCurve; + final bool Function()? draggable; + final RxBase? draggableRx; + final Widget? onTopWidget; const FadeDismissible({ - required super.key, + required Key key, required this.child, - this.onDismissed, - this.onUpdate, + required this.onDismissed, + this.onDismissStart, + this.onDismissEnd, this.direction = DismissDirection.horizontal, - }); + this.dismissDuration = const Duration(milliseconds: 300), + this.settleDuration = const Duration(milliseconds: 300), + this.dismissThreshold = 0.8, + this.dismissRangeStart = 0.1, + this.dismissRangeEnd = 0.9, + this.dismissCurve = Curves.fastOutSlowIn, + this.settleCurve = Curves.easeOutQuart, + this.draggable, + this.draggableRx, + this.onTopWidget, + }) : super(key: key); @override State createState() => _FadeDismissibleState(); } -class _FadeDismissibleState extends State { - final fadeOpacity = ValueNotifier(0.0); +class _FadeDismissibleState extends State with SingleTickerProviderStateMixin { + double get progress => _animation.value.abs(); + + late final _animation = AnimationController( + vsync: this, + lowerBound: -1, + upperBound: 1, + value: 0, + ); @override - Widget build(BuildContext context) { - return Dismissible( - key: widget.key!, - onDismissed: widget.onDismissed, - onUpdate: (details) { - fadeOpacity.value = details.progress; - if (widget.onUpdate != null) widget.onUpdate!(details); - }, - direction: widget.direction, - child: ValueListenableBuilder( - valueListenable: fadeOpacity, - child: widget.child, - builder: (context, value, child) => NamidaOpacity( - opacity: 1 - fadeOpacity.value, - child: child!, - ), + void dispose() { + _animation.dispose(); + super.dispose(); + } + + double _dragged = 0; + + bool _draggable = true; + bool _inDismissRange = true; + + void calculateInDismissRange(double positionDx, double maxWidth) { + final percentage = positionDx / maxWidth; + _inDismissRange = percentage >= widget.dismissRangeStart && percentage <= widget.dismissRangeEnd; + } + + Future _animateDismiss(double to, {required bool faster}) async { + await _animation.animateTo(to, duration: faster ? widget.dismissDuration * 0.5 : widget.dismissDuration, curve: faster ? Curves.linear : widget.dismissCurve); + } + + Future _dismissToRight(DragEndDetails d, {bool faster = false}) async { + await _animateDismiss(1, faster: faster); + widget.onDismissed(DismissDirection.horizontal); + if (widget.onDismissEnd != null) widget.onDismissEnd!(d); + _animation.animateTo(0, duration: Duration.zero); // fixes rendering issue + } + + Future _dismissToLeft(DragEndDetails d, {bool faster = false}) async { + await _animateDismiss(-1, faster: faster); + widget.onDismissed(DismissDirection.horizontal); + if (widget.onDismissEnd != null) widget.onDismissEnd!(d); + _animation.animateTo(0, duration: Duration.zero); // fixes rendering issue + } + + Future _resetToMiddle(DragEndDetails d) async { + await _animation.animateTo(0, duration: widget.settleDuration, curve: widget.settleCurve); + if (widget.onDismissEnd != null) widget.onDismissEnd!(d); + } + + Widget buildChild(bool draggable, Widget child, double maxWidth) { + return HorizontalDragDetector( + onStart: !draggable + ? null + : (d) { + if (widget.onDismissStart != null) widget.onDismissStart!(d); + calculateInDismissRange(d.localPosition.dx, maxWidth); + if (widget.draggable != null) _draggable = widget.draggable!(); + }, + onUpdate: !draggable + ? null + : (d) { + if (!_draggable) return; + if (!_inDismissRange) return; + _dragged += d.delta.dx; + _animation.animateTo(_dragged / maxWidth, duration: Duration.zero); + }, + onEnd: !draggable + ? null + : (d) { + final velocity = d.velocity.pixelsPerSecond.dx; + if (velocity > 800) { + _dismissToRight(d, faster: true); + } else if (velocity < -800) { + _dismissToLeft(d, faster: true); + } else if (progress > widget.dismissThreshold) { + if (_animation.value < 0) { + _dismissToLeft(d); + } else { + _dismissToRight(d); + } + } else { + _resetToMiddle(d); + } + _dragged = 0; + }, + child: AnimatedBuilder( + animation: _animation, + builder: (context, _) { + final p = _animation.value; + if (p == 0) return child; + return Transform.translate( + offset: Offset(p * maxWidth, 0), + child: Opacity( + opacity: 1 - p.abs(), + child: child, + ), + ); + }, ), ); } + + @override + Widget build(BuildContext context) { + final maxWidth = context.width; + final child = widget.onTopWidget != null + ? Stack( + children: [ + widget.child, + widget.onTopWidget!, + ], + ) + : widget.child; + return widget.draggableRx != null + ? ObxO( + rx: widget.draggableRx!, + builder: (value) => buildChild(value && widget.direction != DismissDirection.none, child, maxWidth), + ) + : buildChild(_draggable && widget.direction != DismissDirection.none, child, maxWidth); + } } class NamidaSelectableAutoLinkText extends StatelessWidget { @@ -2081,15 +2205,15 @@ class NamidaSelectableAutoLinkText extends StatelessWidget { Widget build(BuildContext context) { return SelectableAutoLinkText( text, - style: context.textTheme.displayMedium?.copyWith(fontSize: 13.5.multipliedFontScale), + style: context.textTheme.displayMedium?.copyWith(fontSize: 13.5), linkStyle: context.textTheme.displayMedium?.copyWith( color: context.theme.colorScheme.primary.withAlpha(210), - fontSize: 13.5.multipliedFontScale, + fontSize: 13.5, ), highlightedLinkStyle: TextStyle( color: context.theme.colorScheme.primary.withAlpha(220), - backgroundColor: context.theme.colorScheme.onBackground.withAlpha(40), - fontSize: 13.5.multipliedFontScale, + backgroundColor: context.theme.colorScheme.onSurface.withAlpha(40), + fontSize: 13.5, ), scrollPhysics: const NeverScrollableScrollPhysics(), onTap: (url) async => await NamidaLinkUtils.openLink(url), @@ -2156,34 +2280,44 @@ class DefaultPlaylistCard extends StatelessWidget { class NamidaCircularPercentage extends StatelessWidget { final double size; final double percentage; - const NamidaCircularPercentage({super.key, this.size = 48.0, required this.percentage}); + final String heroTag; + + const NamidaCircularPercentage({ + super.key, + this.size = 48.0, + required this.percentage, + required this.heroTag, + }); @override Widget build(BuildContext context) { return Stack( alignment: Alignment.center, children: [ - SleekCircularSlider( - appearance: CircularSliderAppearance( - customWidths: CustomSliderWidths( - trackWidth: size / 24, - progressBarWidth: size / 12, - ), - customColors: CustomSliderColors( - dotColor: Colors.transparent, - trackColor: context.theme.cardTheme.color, - dynamicGradient: true, - progressBarColors: [ - context.theme.colorScheme.primary.withAlpha(100), - Colors.transparent, - context.theme.colorScheme.secondary.withAlpha(100), - Colors.transparent, - context.theme.colorScheme.primary.withAlpha(100), - ], - hideShadow: true, + Hero( + tag: heroTag, + child: SleekCircularSlider( + appearance: CircularSliderAppearance( + customWidths: CustomSliderWidths( + trackWidth: size / 24, + progressBarWidth: size / 12, + ), + customColors: CustomSliderColors( + dotColor: Colors.transparent, + trackColor: context.theme.cardTheme.color, + dynamicGradient: true, + progressBarColors: [ + context.theme.colorScheme.primary.withAlpha(100), + Colors.transparent, + context.theme.colorScheme.secondary.withAlpha(100), + Colors.transparent, + context.theme.colorScheme.primary.withAlpha(100), + ], + hideShadow: true, + ), + size: size, + spinnerMode: true, ), - size: size, - spinnerMode: true, ), ), if (percentage.isFinite) @@ -2204,10 +2338,9 @@ class NamidaListView extends StatelessWidget { final Widget? header; final List? widgetsInColumn; final EdgeInsets? padding; - final List? itemExtents; + final double? itemExtent; final ScrollController? scrollController; final int itemCount; - final bool buildDefaultDragHandles; final ScrollPhysics? physics; final Map scrollConfig; @@ -2219,9 +2352,8 @@ class NamidaListView extends StatelessWidget { this.onReorder, required this.itemBuilder, required this.itemCount, - required this.itemExtents, + required this.itemExtent, this.scrollController, - this.buildDefaultDragHandles = true, this.onReorderStart, this.onReorderEnd, this.physics, @@ -2234,16 +2366,17 @@ class NamidaListView extends StatelessWidget { scrollController: scrollController, scrollConfig: scrollConfig, header: header, - listBuilder: (list) => Column( - children: [ - if (widgetsInColumn != null) ...widgetsInColumn!, - Expanded(child: list), - ], - ), + listBuilder: (list) => widgetsInColumn != null + ? Column( + children: [ + ...widgetsInColumn!, + Expanded(child: list), + ], + ) + : list, itemBuilder: itemBuilder, itemCount: itemCount, - buildDefaultDragHandles: buildDefaultDragHandles, - itemExtents: itemExtents, + itemExtent: itemExtent, onReorder: onReorder, onReorderStart: onReorderStart, onReorderEnd: onReorderEnd, @@ -2260,14 +2393,16 @@ class NamidaListViewRaw extends StatelessWidget { final void Function(int index)? onReorderStart; final void Function(int index)? onReorderEnd; final Widget? header; + final Widget? footer; final EdgeInsets? padding; - final List? itemExtents; + final double? itemExtent; final ScrollController? scrollController; final int itemCount; - final bool buildDefaultDragHandles; final ScrollPhysics? physics; final Map scrollConfig; final double scrollStep; + final Axis scrollDirection; + final bool reverse; const NamidaListViewRaw({ super.key, @@ -2277,53 +2412,122 @@ class NamidaListViewRaw extends StatelessWidget { this.onReorderStart, this.onReorderEnd, this.header, + this.footer, this.padding, - this.itemExtents, + this.itemExtent, this.scrollController, required this.itemCount, - this.buildDefaultDragHandles = true, this.physics, this.scrollConfig = const {}, this.scrollStep = 0, + this.scrollDirection = Axis.vertical, + this.reverse = false, }); @override Widget build(BuildContext context) { - final listW = itemExtents != null - ? KnownExtentsReorderableListView.builder( - itemExtents: itemExtents!, - scrollController: scrollController, - padding: padding ?? EdgeInsets.only(bottom: Dimensions.inst.globalBottomPaddingTotal), - itemBuilder: itemBuilder, - itemCount: itemCount, - onReorder: onReorder ?? (oldIndex, newIndex) {}, - proxyDecorator: (child, index, animation) => child, - header: header, - buildDefaultDragHandles: buildDefaultDragHandles, - physics: physics, - onReorderStart: onReorderStart, - onReorderEnd: onReorderEnd, - ) - : ReorderableListView.builder( - itemExtent: itemExtents?.firstOrNull, - scrollController: scrollController, - padding: padding ?? EdgeInsets.only(bottom: Dimensions.inst.globalBottomPaddingTotal), - itemBuilder: itemBuilder, - itemCount: itemCount, - onReorder: onReorder ?? (oldIndex, newIndex) {}, - proxyDecorator: (child, index, animation) => child, - onReorderStart: onReorderStart, - onReorderEnd: onReorderEnd, - header: header, - buildDefaultDragHandles: onReorder != null, - physics: physics, - ); - return AnimationLimiter( - child: NamidaScrollbar( - controller: scrollController, - scrollStep: scrollStep, - child: listBuilder(listW), - ), + double? start = header == null ? null : 0.0; + double? end = footer == null ? null : 0.0; + if (reverse) { + (start, end) = (end, start); + } + + final padding = this.padding ?? EdgeInsets.only(bottom: Dimensions.inst.globalBottomPaddingTotalR); + final EdgeInsets startPadding, endPadding, listPadding; + (startPadding, endPadding, listPadding) = switch (scrollDirection) { + Axis.horizontal || Axis.vertical when (start ?? end) == null => (EdgeInsets.zero, EdgeInsets.zero, padding), + Axis.horizontal => (padding.copyWith(left: 0), padding.copyWith(right: 0), padding.copyWith(left: start, right: end)), + Axis.vertical => (padding.copyWith(top: 0), padding.copyWith(bottom: 0), padding.copyWith(top: start, bottom: end)), + }; + final (EdgeInsets headerPadding, EdgeInsets footerPadding) = reverse ? (startPadding, endPadding) : (endPadding, startPadding); + + final listW = CustomScrollView( + scrollDirection: scrollDirection, + controller: scrollController, + physics: physics, + reverse: reverse, + slivers: [ + if (header != null) + SliverPadding( + padding: headerPadding, + sliver: SliverToBoxAdapter(child: header), + ), + SliverPadding( + padding: listPadding, + sliver: onReorder != null + ? NamidaSliverReorderableList( + itemExtent: itemExtent, + itemBuilder: itemBuilder, + itemCount: itemCount, + onReorder: onReorder!, + onReorderStart: onReorderStart, + onReorderEnd: onReorderEnd, + ) + : itemExtent != null + ? SliverFixedExtentList.builder( + itemExtent: itemExtent!, + itemBuilder: itemBuilder, + itemCount: itemCount, + ) + : SliverList.builder( + itemBuilder: itemBuilder, + itemCount: itemCount, + ), + ), + if (footer != null) + SliverPadding( + padding: footerPadding, + sliver: SliverToBoxAdapter(child: footer), + ), + ], + ); + return NamidaScrollbar( + controller: scrollController, + scrollStep: scrollStep, + child: listBuilder(listW), + ); + } +} + +class NamidaSliverReorderableList extends StatelessWidget { + final Widget Function(BuildContext context, int i) itemBuilder; + final void Function(int oldIndex, int newIndex)? onReorder; + final void Function(int index)? onReorderStart; + final void Function(int index)? onReorderEnd; + final double? itemExtent; + final int itemCount; + + const NamidaSliverReorderableList({ + super.key, + required this.itemBuilder, + this.onReorder, + this.onReorderStart, + this.onReorderEnd, + this.itemExtent, + required this.itemCount, + }); + + Widget _reorderableItemBuilder(BuildContext context, int index) { + final Widget item = itemBuilder(context, index); + return ReorderableDelayedDragStartListener( + delay: kLongPressTimeout, + key: item.key!, + index: index, + child: item, + ); + } + + @override + Widget build(BuildContext context) { + return SliverReorderableList( + itemExtent: itemExtent, + itemBuilder: _reorderableItemBuilder, + itemCount: itemCount, + onReorder: onReorder!, + proxyDecorator: (child, index, animation) => child, + onReorderStart: onReorderStart, + onReorderEnd: onReorderEnd, + autoScrollerVelocityScalar: 600, ); } } @@ -2338,7 +2542,7 @@ class NamidaTracksList extends StatelessWidget { final EdgeInsetsGeometry? paddingAfterHeader; final ScrollController? scrollController; final EdgeInsets? padding; - final bool isTrackSelectable; + final bool Function()? isTrackSelectable; final ScrollPhysics? physics; final QueueSource queueSource; final bool displayTrackNumber; @@ -2357,7 +2561,7 @@ class NamidaTracksList extends StatelessWidget { this.scrollController, this.padding, required this.queueLength, - this.isTrackSelectable = true, + this.isTrackSelectable, this.physics, required this.queueSource, this.displayTrackNumber = false, @@ -2368,36 +2572,38 @@ class NamidaTracksList extends StatelessWidget { @override Widget build(BuildContext context) { - return NamidaListView( - header: header, - widgetsInColumn: widgetsInColumn, - scrollController: scrollController, - itemCount: queueLength, - itemExtents: List.filled(queueLength, Dimensions.inst.trackTileItemExtent), - padding: padding, - physics: physics, - scrollConfig: scrollConfig, - itemBuilder: itemBuilder ?? - (context, i) { - if (queue != null) { - final track = queue![i]; - return AnimatingTile( - key: ValueKey(i), - position: i, - shouldAnimate: shouldAnimate, - child: TrackTile( - index: i, - trackOrTwd: track, - draggableThumbnail: false, - selectable: isTrackSelectable, - queueSource: queueSource, - displayTrackNumber: displayTrackNumber, - thirdLineText: thirdLineText == null ? '' : thirdLineText!(track), - ), - ); - } - return const Text('PASS A QUEUE OR USE ITEM BUILDER'); - }, + return AnimationLimiter( + child: NamidaListView( + header: header, + widgetsInColumn: widgetsInColumn, + scrollController: scrollController, + itemCount: queueLength, + itemExtent: Dimensions.inst.trackTileItemExtent, + padding: padding, + physics: physics, + scrollConfig: scrollConfig, + itemBuilder: itemBuilder ?? + (context, i) { + if (queue != null) { + final track = queue![i]; + return AnimatingTile( + key: ValueKey(i), + position: i, + shouldAnimate: shouldAnimate, + child: TrackTile( + index: i, + trackOrTwd: track, + draggableThumbnail: false, + selectable: isTrackSelectable, + queueSource: queueSource, + displayTrackNumber: displayTrackNumber, + thirdLineText: thirdLineText == null ? '' : thirdLineText!(track), + ), + ); + } + return const Text('PASS A QUEUE OR USE ITEM BUILDER'); + }, + ), ); } } @@ -2531,7 +2737,7 @@ class NamidaInkWellButton extends StatelessWidget { @override Widget build(BuildContext context) { - final itemsColor = context.theme.colorScheme.onBackground.withOpacity(0.8); + final itemsColor = context.theme.colorScheme.onSurface.withOpacity(0.8); return IgnorePointer( ignoring: !enabled && disableWhenLoading, child: AnimatedOpacity( @@ -2561,7 +2767,7 @@ class NamidaInkWellButton extends StatelessWidget { text, style: context.textTheme.displayMedium?.copyWith( color: itemsColor, - fontSize: (15.0 * sizeMultiplier).multipliedFontScale, + fontSize: (15.0 * sizeMultiplier), ), ), const SizedBox(width: 4.0), @@ -2594,12 +2800,7 @@ class HistoryJumpToDayIcon extends StatelessWidget { final dayToScrollTo = dates.firstOrNull?.toDaysSince1970() ?? 0; final days = controller.historyDays.toList(); days.removeWhere((element) => element <= dayToScrollTo); - controller.canUpdateAllItemsExtentsInHistory = true; - controller.calculateAllItemsExtentsInHistory(); - final itemExtents = controller.allItemsExtentsHistory; - controller.canUpdateAllItemsExtentsInHistory = false; - double totalScrollOffset = 0; - days.loop((e, index) => totalScrollOffset += itemExtents[index]); + double totalScrollOffset = controller.daysToSectionExtent(days); controller.scrollController.jumpTo(totalScrollOffset + 100.0); }, ); @@ -2661,13 +2862,13 @@ class _BetweenDatesTextButtonState extends State { textWidget, const SizedBox(width: 6.0), if (widget.tracksLength != 0) - Text( + NamidaButtonText( "(${widget.tracksLength.displayTrackKeyword})", style: context.textTheme.displaySmall, ), ], ), - Text( + NamidaButtonText( "${oldestDate?.millisecondsSinceEpoch.dateFormattedOriginal} → ${newestDate?.millisecondsSinceEpoch.dateFormattedOriginal}", style: context.textTheme.displaySmall, ), @@ -2679,7 +2880,7 @@ class _BetweenDatesTextButtonState extends State { /// Obx(() => showIf.value ? child : const SizedBox()); class ObxShow extends StatelessWidget { - final RxBool showIf; + final RxBase showIf; final Widget child; const ObxShow({ @@ -2689,7 +2890,12 @@ class ObxShow extends StatelessWidget { }); @override - Widget build(BuildContext context) => Obx(() => showIf.value ? child : const SizedBox()); + Widget build(BuildContext context) { + return ObxO( + rx: showIf, + builder: (show) => show ? child : const SizedBox(), + ); + } } class NamidaHero extends StatelessWidget { @@ -2971,12 +3177,9 @@ class NamidaPopupWrapper extends StatelessWidget { Offset.zero & overlay.size, ); await NamidaNavigator.inst.showMenu( - showMenu( - useRootNavigator: useRootNavigator, - context: context, - position: position, - items: convertItems(context), - ), + context: context, + position: position, + items: convertItems(context), ); if (context.mounted) { popMenu(handleClosing: false); @@ -2985,17 +3188,19 @@ class NamidaPopupWrapper extends StatelessWidget { @override Widget build(BuildContext context) { - return GestureDetector( + return TapDetector( onTap: () { if (onTap != null) onTap!(); if (openOnTap) { _showPopupMenu(context); } }, - onLongPress: openOnLongPress ? () => _showPopupMenu(context) : null, - child: ColoredBox( - color: Colors.transparent, - child: child, + child: LongPressDetector( + onLongPress: openOnLongPress ? () => _showPopupMenu(context) : null, + child: ColoredBox( + color: Colors.transparent, + child: child, + ), ), ); } @@ -3294,11 +3499,11 @@ class _NamidaAZScrollbarState extends State { Obx( () => Positioned( right: 14.0, - top: _selectedChar.value.$1 * columnHeight, + top: _selectedChar.valueR.$1 * columnHeight, child: Container( margin: const EdgeInsets.symmetric(horizontal: 8.0), decoration: BoxDecoration(shape: BoxShape.circle, color: context.theme.cardColor), - child: Text(_selectedChar.value.$2), + child: Text(_selectedChar.valueR.$2), ), ), ), @@ -3412,7 +3617,7 @@ class QueueUtilsRow extends StatelessWidget { const SizedBox(width: 6.0), NamidaButton( tooltip: lang.REMOVE_DUPLICATES, - icon: Broken.trash, + icon: Broken.broom, onPressed: () { final removed = Player.inst.removeDuplicatesFromQueue(); snackyy( @@ -3432,7 +3637,28 @@ class QueueUtilsRow extends StatelessWidget { const SizedBox(width: 6.0), GestureDetector( onLongPressStart: (details) async { - void saveSetting(bool shuffleAll) => settings.player.save(shuffleAllTracks: shuffleAll); + Widget buildButton(String title, IconData icon, bool isShuffleAll) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: tileVPadding), + child: ObxO( + rx: settings.player.shuffleAllTracks, + builder: (shuffleAllTracks) => SizedBox( + height: tileHeight, + child: ListTileWithCheckMark( + active: shuffleAllTracks == isShuffleAll, + leading: StackedIcon( + baseIcon: Broken.shuffle, + secondaryIcon: icon, + blurRadius: 8.0, + ), + title: title, + onTap: () => settings.player.save(shuffleAllTracks: isShuffleAll), + ), + ), + ), + ); + } + await showMenu( context: context, position: RelativeRect.fromLTRB( @@ -3442,38 +3668,11 @@ class QueueUtilsRow extends StatelessWidget { details.globalPosition.dy, ), items: [ - ...[ - ( - lang.SHUFFLE_NEXT, - Broken.forward, - false, - ), - ( - lang.SHUFFLE_ALL, - Broken.task, - true, - ), - ].map( - (e) => PopupMenuItem( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: tileVPadding), - child: Obx( - () => SizedBox( - height: tileHeight, - child: ListTileWithCheckMark( - active: settings.player.shuffleAllTracks.value == e.$3, - leading: StackedIcon( - baseIcon: Broken.shuffle, - secondaryIcon: e.$2, - blurRadius: 8.0, - ), - title: e.$1, - onTap: () => saveSetting(e.$3), - ), - ), - ), - ), - ), + PopupMenuItem( + child: buildButton(lang.SHUFFLE_NEXT, Broken.forward, false), + ), + PopupMenuItem( + child: buildButton(lang.SHUFFLE_ALL, Broken.task, true), ), ], ); @@ -3510,48 +3709,64 @@ class RepeatModeIconButton extends StatelessWidget { @override Widget build(BuildContext context) { final iconColor = color ?? context.theme.colorScheme.onSecondaryContainer; - return Obx( - () { - final tooltip = settings.player.repeatMode.value.toText().replaceFirst('_NUM_', Player.inst.numberOfRepeats.toString()); - final child = Stack( - alignment: Alignment.center, - children: [ - Icon( - settings.player.repeatMode.value.toIcon(), - size: 20.0, - color: iconColor, - ), - if (settings.player.repeatMode.value == RepeatMode.forNtimes) - Text( - Player.inst.numberOfRepeats.toString(), - style: context.textTheme.displaySmall?.copyWith(color: iconColor), - ), - ], + + return ObxO( + rx: settings.player.repeatMode, + builder: (repeatMode) { + final icon = repeatMode.toIcon(); + String tooltip = repeatMode.toText(); + + return ObxO( + rx: Player.inst.numberOfRepeats, + builder: (numberOfRepeats) { + String? numberOfRepeatsText; + if (repeatMode == RepeatMode.forNtimes) { + numberOfRepeatsText = numberOfRepeats.toString(); + tooltip = tooltip.replaceFirst('_NUM_', numberOfRepeatsText); + } + + final child = Stack( + alignment: Alignment.center, + children: [ + Icon( + icon, + size: 20.0, + color: iconColor, + ), + if (numberOfRepeatsText != null) + Text( + numberOfRepeatsText, + style: context.textTheme.displaySmall?.copyWith(color: iconColor), + ), + ], + ); + + return compact + ? NamidaIconButton( + tooltip: tooltip, + icon: null, + horizontalPadding: 0.0, + padding: EdgeInsets.zero, + iconSize: 20.0, + onPressed: () { + onPressed?.call(); + _switchMode(); + }, + child: child, + ) + : IconButton( + visualDensity: VisualDensity.compact, + style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), + padding: const EdgeInsets.all(2.0), + tooltip: tooltip, + onPressed: () { + onPressed?.call(); + _switchMode(); + }, + icon: child, + ); + }, ); - return compact - ? NamidaIconButton( - tooltip: tooltip, - icon: null, - horizontalPadding: 0.0, - padding: EdgeInsets.zero, - iconSize: 20.0, - onPressed: () { - onPressed?.call(); - _switchMode(); - }, - child: child, - ) - : IconButton( - visualDensity: VisualDensity.compact, - style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), - padding: const EdgeInsets.all(2.0), - tooltip: tooltip, - onPressed: () { - onPressed?.call(); - _switchMode(); - }, - icon: child, - ); }, ); } @@ -3582,7 +3797,7 @@ class EqualizerIconButton extends StatelessWidget { builder: (context, snapshot) { return Obx( () { - final isSoundModified = settings.player.speed.value != 1.0 || settings.player.pitch.value != 1.0; + final isSoundModified = settings.player.speed.valueR != 1.0 || settings.player.pitch.valueR != 1.0; final enabled = isSoundModified || (snapshot.data ?? false); return enabled ? StackedIcon( @@ -3774,6 +3989,50 @@ class ScaleDetector extends StatelessWidget { } } +class HorizontalDragDetector extends StatelessWidget { + final GestureDragStartCallback? onStart; + final GestureDragDownCallback? onDown; + final GestureDragUpdateCallback? onUpdate; + final GestureDragEndCallback? onEnd; + final void Function(HorizontalDragGestureRecognizer instance)? initializer; + final Widget? child; + final HitTestBehavior? behavior; + + const HorizontalDragDetector({ + super.key, + this.initializer, + this.child, + this.behavior, + this.onStart, + this.onDown, + this.onUpdate, + this.onEnd, + }); + + @override + Widget build(BuildContext context) { + final Map gestures = {}; + gestures[HorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers( + () => HorizontalDragGestureRecognizer(debugOwner: this), + initializer ?? + (HorizontalDragGestureRecognizer instance) { + instance + ..onStart = onStart + ..onDown = onDown + ..onUpdate = onUpdate + ..onEnd = onEnd + ..gestureSettings = MediaQuery.maybeGestureSettingsOf(context); + }, + ); + + return RawGestureDetector( + behavior: behavior, + gestures: gestures, + child: child, + ); + } +} + class DecorationClipper extends CustomClipper { const DecorationClipper({ this.textDirection = TextDirection.ltr, @@ -3819,3 +4078,62 @@ class BorderRadiusClip extends StatelessWidget { ); } } + +class NamidaHistoryDayHeaderBox extends StatelessWidget { + final double height; + final String title; + final Widget menu; + final Color bgColor; + final Color sideColor; + final Color shadowColor; + + const NamidaHistoryDayHeaderBox({ + super.key, + required this.height, + required this.title, + required this.menu, + required this.sideColor, + required this.bgColor, + required this.shadowColor, + }); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: bgColor, + border: Border( + left: BorderSide( + color: sideColor, + width: 4.0, + ), + ), + boxShadow: [ + BoxShadow( + offset: const Offset(0, 2.0), + blurRadius: 4.0, + color: shadowColor, + ), + ], + ), + child: SizedBox( + width: context.width, + height: height, + child: Row( + children: [ + const SizedBox(width: 12.0), + Expanded( + child: Text( + title, + style: context.textTheme.displayMedium, + ), + ), + const SizedBox(width: 4.0), + menu, + const SizedBox(width: 4.0), + ], + ), + ), + ); + } +} diff --git a/lib/ui/widgets/expandable_box.dart b/lib/ui/widgets/expandable_box.dart index 2ee3f79f..fb5ce967 100644 --- a/lib/ui/widgets/expandable_box.dart +++ b/lib/ui/widgets/expandable_box.dart @@ -1,16 +1,15 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - import 'package:namida/controller/scroll_search_controller.dart'; import 'package:namida/core/dimensions.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/animated_widgets.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/settings/extra_settings.dart'; -class ExpandableBox extends StatelessWidget { +class ExpandableBox extends StatefulWidget { final bool isBarVisible; final bool showSearchBox; final bool displayloadingIndicator; @@ -18,7 +17,7 @@ class ExpandableBox extends StatelessWidget { final String leftText; final void Function() onCloseButtonPressed; final SortByMenu sortByMenuWidget; - final CustomTextFiled textField; + final CustomTextFiled Function() textField; final ChangeGridCountWidget? gridWidget; final List? leftWidgets; final bool enableHero; @@ -38,19 +37,50 @@ class ExpandableBox extends StatelessWidget { required this.enableHero, }); + @override + State createState() => _ExpandableBoxState(); +} + +class _ExpandableBoxState extends State with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late bool _latestShowSearchBox; + + @override + void initState() { + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + value: widget.showSearchBox ? 1.0 : 0.0, + ); + _latestShowSearchBox = widget.showSearchBox; + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { + final textfieldWidget = widget.textField(); + if (widget.showSearchBox != _latestShowSearchBox) { + _latestShowSearchBox = widget.showSearchBox; + _controller.animateTo(widget.showSearchBox ? 1.0 : 0.0); + } + return NamidaHero( - enabled: enableHero, + enabled: widget.enableHero, tag: 'ExpandableBox', child: Column( children: [ AnimatedOpacity( - opacity: isBarVisible ? 1 : 0, + opacity: widget.isBarVisible ? 1 : 0, duration: const Duration(milliseconds: 400), child: AnimatedSizedBox( duration: const Duration(milliseconds: 400), - height: isBarVisible ? kExpandableBoxHeight : 0.0, + height: widget.isBarVisible ? kExpandableBoxHeight : 0.0, animateWidth: false, child: Row( mainAxisAlignment: MainAxisAlignment.start, @@ -62,62 +92,60 @@ class ExpandableBox extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - if (leftWidgets != null) ...leftWidgets!, + if (widget.leftWidgets != null) ...widget.leftWidgets!, Expanded( child: Text( - leftText, + widget.leftText, style: context.textTheme.displayMedium, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), - if (displayloadingIndicator) ...[const SizedBox(width: 8.0), const LoadingIndicator()] + if (widget.displayloadingIndicator) ...[const SizedBox(width: 8.0), const LoadingIndicator()] ], ), ), - // const Spacer(), - if (gridWidget != null) gridWidget!, - // Sort By Menu + if (widget.gridWidget != null) widget.gridWidget!, const SizedBox(width: 4.0), - sortByMenuWidget, + widget.sortByMenuWidget, const SizedBox(width: 12.0), SmallIconButton( icon: Broken.filter_search, - onTap: onFilterIconTap, + onTap: widget.onFilterIconTap, ), const SizedBox(width: 12.0), ], ), ), ), - AnimatedOpacity( - opacity: showSearchBox ? 1 : 0, - duration: const Duration(milliseconds: 400), - child: AnimatedSize( - duration: const Duration(milliseconds: 400), - child: AnimatedSizedBox( - duration: const Duration(milliseconds: 400), - height: showSearchBox ? 58.0 : 0, - animateWidth: false, - child: Row( - children: [ - const SizedBox(width: 12.0), - Expanded(child: textField), - const SizedBox(width: 12.0), - NamidaIconButton( - onPressed: () { - onCloseButtonPressed(); - ScrollSearchController.inst.unfocusKeyboard(); - }, - icon: Broken.close_circle, - ), - const SizedBox(width: 8.0), - ], + AnimatedBuilder( + animation: _controller, + child: Row( + children: [ + const SizedBox(width: 12.0), + Expanded(child: textfieldWidget), + const SizedBox(width: 12.0), + NamidaIconButton( + onPressed: () { + widget.onCloseButtonPressed(); + ScrollSearchController.inst.unfocusKeyboard(); + }, + icon: Broken.close_circle, ), - ), + const SizedBox(width: 8.0), + ], ), + builder: (context, child) { + return Opacity( + opacity: _controller.value, + child: SizedBox( + height: _controller.value * 58.0, + child: child!, + ), + ); + }, ), - if (showSearchBox) const SizedBox(height: 8.0) + if (widget.showSearchBox) const SizedBox(height: 8.0) ], ), ); @@ -175,7 +203,7 @@ class SortByMenu extends StatelessWidget { style: const ButtonStyle( visualDensity: VisualDensity.compact, ), - child: Text(title), + child: NamidaButtonText(title, style: const TextStyle(fontSize: 14.5)), onPressed: () => showMenu( color: context.theme.appBarTheme.backgroundColor, context: context, diff --git a/lib/ui/widgets/inner_drawer.dart b/lib/ui/widgets/inner_drawer.dart index c0cc29f6..764baf08 100644 --- a/lib/ui/widgets/inner_drawer.dart +++ b/lib/ui/widgets/inner_drawer.dart @@ -1,11 +1,8 @@ -// ignore_for_file: unused_element - -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/core/constants.dart'; import 'package:namida/core/extensions.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/animated_widgets.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; @@ -40,7 +37,10 @@ class NamidaInnerDrawerState extends State with SingleTickerP void toggle() => isOpened ? _closeDrawer() : _openDrawer(); void open() => _openDrawer(); void close() => _closeDrawer(); - void toggleCanSwipe(bool swipe) => setState(() => _canSwipe = swipe); + void toggleCanSwipe(bool swipe) { + if (_canSwipe == swipe) return; + setState(() => _canSwipe = swipe); + } late final AnimationController controller; @@ -174,7 +174,7 @@ class NamidaInnerDrawerState extends State with SingleTickerP ); return _canSwipe // -- touch absorber - ? _HorizontalDragDetector( + ? HorizontalDragDetector( behavior: HitTestBehavior.translucent, onDown: (details) { controller.stop(); @@ -203,44 +203,3 @@ class NamidaInnerDrawerState extends State with SingleTickerP ); } } - -class _HorizontalDragDetector extends StatelessWidget { - final GestureDragDownCallback? onDown; - final GestureDragUpdateCallback? onUpdate; - final GestureDragEndCallback? onEnd; - final void Function(HorizontalDragGestureRecognizer instance)? initializer; - final Widget? child; - final HitTestBehavior? behavior; - - const _HorizontalDragDetector({ - super.key, - this.initializer, - this.child, - this.behavior, - this.onDown, - this.onUpdate, - this.onEnd, - }); - - @override - Widget build(BuildContext context) { - final Map gestures = {}; - gestures[HorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers( - () => HorizontalDragGestureRecognizer(debugOwner: this), - initializer ?? - (HorizontalDragGestureRecognizer instance) { - instance - ..onDown = onDown - ..onUpdate = onUpdate - ..onEnd = onEnd - ..gestureSettings = MediaQuery.maybeGestureSettingsOf(context); - }, - ); - - return RawGestureDetector( - behavior: behavior, - gestures: gestures, - child: child, - ); - } -} diff --git a/lib/ui/widgets/library/album_card.dart b/lib/ui/widgets/library/album_card.dart index aec5d91c..241954b4 100644 --- a/lib/ui/widgets/library/album_card.dart +++ b/lib/ui/widgets/library/album_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/track.dart'; import 'package:namida/controller/player_controller.dart'; @@ -44,8 +44,8 @@ class AlbumCard extends StatelessWidget { Widget build(BuildContext context) { // final d = Dimensions.inst.albumCardDimensions; final thumbnailSize = dimensions.$1; - final fontSize = dimensions.$2.multipliedFontScale; - final fontSizeBigger = topRightText == null ? null : (dimensions.$2 + (topRightText != null ? 3.0 : 0.0)).multipliedFontScale; + final fontSize = dimensions.$2; + final fontSizeBigger = topRightText == null ? null : (dimensions.$2 + (topRightText != null ? 3.0 : 0.0)); final sizeAlternative = dimensions.$3; final finalYear = album.year.yearFormatted; diff --git a/lib/ui/widgets/library/album_tile.dart b/lib/ui/widgets/library/album_tile.dart index a1471eb0..b6b917e3 100644 --- a/lib/ui/widgets/library/album_tile.dart +++ b/lib/ui/widgets/library/album_tile.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/track.dart'; import 'package:namida/controller/settings_controller.dart'; diff --git a/lib/ui/widgets/library/artist_card.dart b/lib/ui/widgets/library/artist_card.dart index 1d1824a9..a0b428d3 100644 --- a/lib/ui/widgets/library/artist_card.dart +++ b/lib/ui/widgets/library/artist_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/track.dart'; import 'package:namida/core/dimensions.dart'; @@ -36,7 +36,7 @@ class ArtistCard extends StatelessWidget { @override Widget build(BuildContext context) { final thumbnailSize = dimensions.$1; - final fontSize = dimensions.$2.multipliedFontScale; + final fontSize = dimensions.$2; final hero = 'artist_$name$additionalHeroTag'; return GridTile( diff --git a/lib/ui/widgets/library/artist_tile.dart b/lib/ui/widgets/library/artist_tile.dart index 767965a3..e6ad9df6 100644 --- a/lib/ui/widgets/library/artist_tile.dart +++ b/lib/ui/widgets/library/artist_tile.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/track.dart'; import 'package:namida/core/dimensions.dart'; @@ -76,7 +76,7 @@ class ArtistTile extends StatelessWidget { tracks.displayTrackKeyword, albums.length.displayAlbumKeyword, ].join(' & '), - style: context.textTheme.displaySmall?.copyWith(fontSize: 14.0.multipliedFontScale), + style: context.textTheme.displaySmall?.copyWith(fontSize: 14.0), overflow: TextOverflow.ellipsis, ), ), diff --git a/lib/ui/widgets/library/folder_tile.dart b/lib/ui/widgets/library/folder_tile.dart index 8611a804..6981bcbb 100644 --- a/lib/ui/widgets/library/folder_tile.dart +++ b/lib/ui/widgets/library/folder_tile.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/folder.dart'; import 'package:namida/class/track.dart'; @@ -28,7 +28,7 @@ class FolderTile extends StatelessWidget { @override Widget build(BuildContext context) { final dirInside = folder.getDirectoriesInside(); - final tracks = dummyTracks ?? folder.tracks; + final tracks = dummyTracks ?? folder.tracks(); final double iconSize = (settings.trackThumbnailSizeinList.value / 1.35).clamp(0, settings.trackListTileHeight.value); final double thumbSize = (settings.trackThumbnailSizeinList.value / 2.6).clamp(0, settings.trackListTileHeight.value * 0.5); return Padding( diff --git a/lib/ui/widgets/library/multi_artwork_card.dart b/lib/ui/widgets/library/multi_artwork_card.dart index d0fd965a..05878591 100644 --- a/lib/ui/widgets/library/multi_artwork_card.dart +++ b/lib/ui/widgets/library/multi_artwork_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/track.dart'; import 'package:namida/core/dimensions.dart'; @@ -33,7 +33,7 @@ class MultiArtworkCard extends StatelessWidget { @override Widget build(BuildContext context) { final thumbnailSize = dimensions.$1; - final fontSize = dimensions.$2.multipliedFontScale; + final fontSize = dimensions.$2; return GridTile( child: Container( diff --git a/lib/ui/widgets/library/multi_artwork_container.dart b/lib/ui/widgets/library/multi_artwork_container.dart index 2c79a859..d1f56c59 100644 --- a/lib/ui/widgets/library/multi_artwork_container.dart +++ b/lib/ui/widgets/library/multi_artwork_container.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/track.dart'; import 'package:namida/core/extensions.dart'; diff --git a/lib/ui/widgets/library/playlist_tile.dart b/lib/ui/widgets/library/playlist_tile.dart index 5db95294..8ff18fbe 100644 --- a/lib/ui/widgets/library/playlist_tile.dart +++ b/lib/ui/widgets/library/playlist_tile.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/track.dart'; import 'package:namida/controller/playlist_controller.dart'; @@ -38,6 +38,7 @@ class PlaylistTile extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: Dimensions.tileVerticalPadding), child: Obx( () { + PlaylistController.inst.playlistsMap.valueR; final playlist = PlaylistController.inst.getPlaylist(playlistName); if (playlist == null) return const SizedBox(); final tracksRaw = playlist.tracks.toTracks(); @@ -66,7 +67,7 @@ class PlaylistTile extends StatelessWidget { tag: 'line2_$hero', child: Text( [tracksRaw.displayTrackKeyword, playlist.creationDate.dateFormatted].join(' • '), - style: context.textTheme.displaySmall?.copyWith(fontSize: 13.7.multipliedFontScale), + style: context.textTheme.displaySmall?.copyWith(fontSize: 13.7), overflow: TextOverflow.ellipsis, ), ), diff --git a/lib/ui/widgets/library/queue_tile.dart b/lib/ui/widgets/library/queue_tile.dart index 7d1b83b8..79136dda 100644 --- a/lib/ui/widgets/library/queue_tile.dart +++ b/lib/ui/widgets/library/queue_tile.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/queue.dart'; import 'package:namida/controller/navigator_controller.dart'; @@ -53,12 +53,9 @@ class QueueTile extends StatelessWidget { title: lang.UNDO_CHANGES, message: lang.UNDO_CHANGES_DELETED_QUEUE, displaySeconds: 3, - button: TextButton( - onPressed: () { - QueueController.inst.reAddQueue(oldQueue); - Get.closeAllSnackbars(); - }, - child: Text(lang.UNDO), + button: ( + lang.UNDO, + () async => await QueueController.inst.reAddQueue(oldQueue), ), ); }, @@ -89,7 +86,7 @@ class QueueTile extends StatelessWidget { child: Text( queue.date.dateAndClockFormattedOriginal, style: context.textTheme.displayMedium?.copyWith( - fontSize: 14.0.multipliedFontScale, + fontSize: 14.0, ), overflow: TextOverflow.ellipsis, ), @@ -112,7 +109,7 @@ class QueueTile extends StatelessWidget { queue.tracks.totalDurationFormatted, style: context.textTheme.displaySmall?.copyWith( fontWeight: FontWeight.w500, - fontSize: 12.5.multipliedFontScale, + fontSize: 12.5, ), overflow: TextOverflow.ellipsis, ), diff --git a/lib/ui/widgets/library/track_tile.dart b/lib/ui/widgets/library/track_tile.dart index 91e99dfa..6144f378 100644 --- a/lib/ui/widgets/library/track_tile.dart +++ b/lib/ui/widgets/library/track_tile.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/class/track.dart'; import 'package:namida/controller/current_color.dart'; @@ -32,7 +32,7 @@ class TrackTile extends StatelessWidget { final bool displayTrackNumber; /// Disable if you want to have priority to hold & reorder instead of selecting. - final bool selectable; + final bool Function()? selectable; final QueueSource queueSource; final void Function()? onRightAreaTap; final double cardColorOpacity; @@ -51,7 +51,7 @@ class TrackTile extends StatelessWidget { required this.index, this.thirdLineText = '', this.displayTrackNumber = false, - this.selectable = true, + this.selectable, required this.queueSource, this.onRightAreaTap, this.cardColorOpacity = 0.9, @@ -104,14 +104,18 @@ class TrackTile extends StatelessWidget { children: [ Obx( () { - final willSleepAfterThis = queueSource == QueueSource.playerQueue && Player.inst.enableSleepAfterTracks && Player.inst.sleepingTrackIndex == index; + bool willSleepAfterThis = false; + if (queueSource == QueueSource.playerQueue) { + final sleepconfig = Player.inst.sleepTimerConfig.valueR; + willSleepAfterThis = sleepconfig.enableSleepAfterItems && Player.inst.sleepingItemIndex(sleepconfig.sleepAfterItems, Player.inst.currentIndex.valueR) == index; + } - final double thumbnailSize = settings.trackThumbnailSizeinList.value; - final double trackTileHeight = settings.trackListTileHeight.value; + final double thumbnailSize = settings.trackThumbnailSizeinList.valueR; + final double trackTileHeight = settings.trackListTileHeight.valueR; final bool isTrackSelected = SelectedTracksController.inst.isTrackSelected(trackOrTwd); - final bool isTrackSame = track == CurrentColor.inst.currentPlayingTrack.value?.track; - final bool isRightHistoryList = queueSource == QueueSource.history ? trackWithDate == CurrentColor.inst.currentPlayingTrack.value?.trackWithDate : true; - final bool isRightIndex = canHaveDuplicates ? index == CurrentColor.inst.currentPlayingIndex.value : true; + final bool isTrackSame = track == CurrentColor.inst.currentPlayingTrack.valueR?.track; + final bool isRightHistoryList = queueSource == QueueSource.history ? trackWithDate == CurrentColor.inst.currentPlayingTrack.valueR?.trackWithDate : true; + final bool isRightIndex = canHaveDuplicates ? index == CurrentColor.inst.currentPlayingIndex.valueR : true; final bool isTrackCurrentlyPlaying = isRightIndex && isTrackSame && isRightHistoryList; final textColor = isTrackCurrentlyPlaying && !isTrackSelected ? Colors.white : null; @@ -127,7 +131,7 @@ class TrackTile extends StatelessWidget { isTrackCurrentlyPlaying ? comingFromQueue ? CurrentColor.inst.miniplayerColor - : CurrentColor.inst.color + : CurrentColor.inst.currentColorScheme : context.theme.cardTheme.color!.withOpacity(cardColorOpacity), ); @@ -138,7 +142,7 @@ class TrackTile extends StatelessWidget { child: InkWell( onTap: onTap ?? () async { - if (SelectedTracksController.inst.selectedTracks.isNotEmpty && !isInSelectedTracksPreview) { + if (SelectedTracksController.inst.selectedTracks.value.isNotEmpty && !isInSelectedTracksPreview) { _selectTrack(); } else { if (onPlaying != null) { @@ -149,7 +153,7 @@ class TrackTile extends StatelessWidget { ScrollSearchController.inst.unfocusKeyboard(); await Player.inst.playOrPause( settings.trackPlayMode.value.shouldBeIndex0 ? 0 : index, - settings.trackPlayMode.value.getQueue(track), + settings.trackPlayMode.value.generateQueue(track), queueSource, ); } else { @@ -161,9 +165,10 @@ class TrackTile extends StatelessWidget { } } }, - onLongPress: !selectable || onTap != null + onLongPress: onTap != null ? null : () { + if (selectable != null && selectable!() == false) return; if (!isInSelectedTracksPreview) { ScrollSearchController.inst.unfocusKeyboard(); _selectTrack(); @@ -195,7 +200,7 @@ class TrackTile extends StatelessWidget { track: track, thumbnailSize: thumbnailSize, path: track.pathToImage, - forceSquared: settings.forceSquaredTrackThumbnail.value, + forceSquared: settings.forceSquaredTrackThumbnail.valueR, useTrackTileCacheHeight: true, onTopWidgets: [ if (displayTrackNumber) @@ -218,7 +223,7 @@ class TrackTile extends StatelessWidget { child: Container( padding: const EdgeInsets.all(2.0), decoration: BoxDecoration( - color: context.theme.colorScheme.background.withAlpha(160), + color: context.theme.colorScheme.surface.withAlpha(160), borderRadius: BorderRadius.circular(12.0.multipliedRadius), ), child: const Icon( @@ -235,7 +240,6 @@ class TrackTile extends StatelessWidget { if (draggableThumbnail) NamidaReordererableListener( durationMs: 80, - isInQueue: queueSource == QueueSource.playerQueue, index: index, child: Container( color: Colors.transparent, @@ -250,7 +254,9 @@ class TrackTile extends StatelessWidget { child: trackOrTwd.track.toTrackExtOrNull() == null ? Text( trackOrTwd.track.path, - style: context.textTheme.displaySmall, + style: context.textTheme.displaySmall?.copyWith( + color: textColor?.withAlpha(170), + ), ) : Column( mainAxisAlignment: MainAxisAlignment.center, @@ -280,7 +286,7 @@ class TrackTile extends StatelessWidget { ), // check if third row isnt empty - if (thirdLineText == '' && settings.displayThirdRow.value) + if (thirdLineText == '' && settings.displayThirdRow.valueR) if (row3Text != '') Text( row3Text, @@ -302,7 +308,7 @@ class TrackTile extends StatelessWidget { ), ), const SizedBox(width: 6.0), - if (settings.displayFavouriteIconInListTile.value || rightItem1Text != '' || rightItem2Text != '') + if (settings.displayFavouriteIconInListTile.valueR || rightItem1Text != '' || rightItem2Text != '') Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -324,7 +330,7 @@ class TrackTile extends StatelessWidget { ), overflow: TextOverflow.ellipsis, ), - if (settings.displayFavouriteIconInListTile.value) + if (settings.displayFavouriteIconInListTile.valueR) NamidaLikeButton( track: track, size: 22.0, @@ -336,7 +342,6 @@ class TrackTile extends StatelessWidget { const SizedBox(width: 8.0), NamidaReordererableListener( durationMs: 20, - isInQueue: queueSource == QueueSource.playerQueue, index: index, child: FittedBox( child: Icon( @@ -402,7 +407,7 @@ class TrackTileManager { if (i1 != '') i1, if (i2 != '') i2, if (i3 != '') i3, - ].join(' ${settings.trackTileSeparator} '); + ].join(' ${settings.trackTileSeparator.value} '); } static String getChoosenTrackTileItem(TrackTilePosition? itemPosition, Track trackPre) { @@ -410,7 +415,7 @@ class TrackTileManager { final inf = _infoMap[trackPre]?[itemPosition]; if (_infoMap[trackPre]?[itemPosition] != null) return inf!; - final trackItem = settings.trackItem[itemPosition] ?? TrackTileItem.none; + final trackItem = settings.trackItem.value[itemPosition] ?? TrackTileItem.none; final val = _getTrackItemValue(trackItem, trackPre); diff --git a/lib/ui/widgets/selected_tracks_preview.dart b/lib/ui/widgets/selected_tracks_preview.dart index d91003f5..5368ebf1 100644 --- a/lib/ui/widgets/selected_tracks_preview.dart +++ b/lib/ui/widgets/selected_tracks_preview.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - import 'package:namida/class/track.dart'; import 'package:namida/controller/player_controller.dart'; import 'package:namida/controller/selected_tracks_controller.dart'; @@ -10,8 +8,8 @@ import 'package:namida/core/dimensions.dart'; import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; -import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/dialogs/add_to_playlist_dialog.dart'; import 'package:namida/ui/dialogs/edit_tags_dialog.dart'; import 'package:namida/ui/dialogs/general_popup_dialog.dart'; @@ -31,11 +29,12 @@ class SelectedTracksPreviewContainer extends StatelessWidget { child: Obx( () { final SelectedTracksController stc = SelectedTracksController.inst; + final selectedTracks = stc.selectedTracks.valueR; return AnimatedSwitcher( duration: const Duration(milliseconds: 300), switchInCurve: Curves.easeOut, switchOutCurve: Curves.easeIn, - child: stc.selectedTracks.isNotEmpty + child: selectedTracks.isNotEmpty ? Center( child: Container( width: context.width, @@ -59,16 +58,16 @@ class SelectedTracksPreviewContainer extends StatelessWidget { child: AnimatedSizedBox( duration: const Duration(seconds: 1), curve: Curves.fastLinearToSlowEaseIn, - height: stc.isMenuMinimized.value - ? stc.isExpanded.value + height: stc.isMenuMinimized.valueR + ? stc.isExpanded.valueR ? 80 : 85 - : stc.isExpanded.value + : stc.isExpanded.valueR ? 425 : 430, - width: stc.isExpanded.value ? 375 : 380, + width: stc.isExpanded.valueR ? 375 : 380, decoration: BoxDecoration( - color: Color.alphaBlend(context.theme.colorScheme.background.withAlpha(160), context.theme.scaffoldBackgroundColor), + color: Color.alphaBlend(context.theme.colorScheme.surface.withAlpha(160), context.theme.scaffoldBackgroundColor), borderRadius: const BorderRadius.all(Radius.circular(20)), boxShadow: [ BoxShadow( @@ -80,7 +79,7 @@ class SelectedTracksPreviewContainer extends StatelessWidget { ), child: Padding( padding: const EdgeInsets.all(16.0), - child: stc.isMenuMinimized.value + child: stc.isMenuMinimized.valueR ? const FittedBox(child: SelectedTracksRow()) : Column( mainAxisSize: MainAxisSize.max, @@ -95,18 +94,18 @@ class SelectedTracksPreviewContainer extends StatelessWidget { clipBehavior: Clip.antiAlias, decoration: BoxDecoration(borderRadius: BorderRadius.circular(12)), child: NamidaListView( - itemExtents: stc.selectedTracks.toTrackItemExtents(), - itemCount: stc.selectedTracks.length, + itemExtent: Dimensions.inst.trackTileItemExtent, + itemCount: selectedTracks.length, onReorder: (oldIndex, newIndex) => stc.reorderTracks(oldIndex, newIndex), padding: EdgeInsets.zero, itemBuilder: (context, i) { - return Dismissible( - key: ValueKey(stc.selectedTracks[i]), + return FadeDismissible( + key: ValueKey(selectedTracks[i]), onDismissed: (direction) => stc.removeTrack(i), child: TrackTile( - key: Key('$i${stc.selectedTracks[i]}'), + key: Key('$i${selectedTracks[i]}'), index: i, - trackOrTwd: stc.selectedTracks[i], + trackOrTwd: selectedTracks[i], displayRightDragHandler: true, queueSource: QueueSource.selectedTracks, ), @@ -156,7 +155,7 @@ class SelectedTracksPreviewContainer extends StatelessWidget { class SelectedTracksRow extends StatelessWidget { const SelectedTracksRow({super.key}); - List get selectedTracks => SelectedTracksController.inst.selectedTracks.tracks.toList(); + List getSelectedTracks() => SelectedTracksController.inst.selectedTracks.value.tracks.toList(); @override Widget build(BuildContext context) { @@ -171,35 +170,39 @@ class SelectedTracksRow extends StatelessWidget { SizedBox( width: 140, child: Obx( - () => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - SelectedTracksController.inst.selectedTracks.displayTrackKeyword, - style: context.theme.textTheme.displayLarge!.copyWith(fontSize: 23.0.multipliedFontScale), - ), - if (!SelectedTracksController.inst.isMenuMinimized.value) + () { + final selectedTracks = SelectedTracksController.inst.selectedTracks.valueR.tracks.toList(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( - selectedTracks.totalDurationFormatted, - style: context.theme.textTheme.displayMedium, - ) - ], - ), + selectedTracks.displayTrackKeyword, + style: context.theme.textTheme.displayLarge!.copyWith(fontSize: 23.0), + ), + if (!SelectedTracksController.inst.isMenuMinimized.valueR) + Text( + selectedTracks.totalDurationFormatted, + style: context.theme.textTheme.displayMedium, + ) + ], + ); + }, ), ), const SizedBox( width: 32, ), - Obx( - () => AnimatedOpacity( + ObxO( + rx: SelectedTracksController.inst.didInsertTracks, + builder: (didInsertTracks) => AnimatedOpacity( duration: const Duration(milliseconds: 400), - opacity: SelectedTracksController.inst.didInsertTracks.value ? 0.5 : 1.0, + opacity: didInsertTracks ? 0.5 : 1.0, child: IgnorePointer( - ignoring: SelectedTracksController.inst.didInsertTracks.value, + ignoring: didInsertTracks, child: IconButton( onPressed: () { SelectedTracksController.inst.didInsertTracks.value = true; - Player.inst.addToQueue(selectedTracks); + Player.inst.addToQueue(getSelectedTracks()); }, icon: const Icon(Broken.play_cricle), tooltip: lang.PLAY_LAST, @@ -208,19 +211,19 @@ class SelectedTracksRow extends StatelessWidget { ), ), IconButton( - onPressed: () => showEditTracksTagsDialog(selectedTracks, null), + onPressed: () => showEditTracksTagsDialog(getSelectedTracks(), null), tooltip: lang.EDIT_TAGS, icon: const Icon(Broken.edit), ), IconButton( - onPressed: () => showAddToPlaylistDialog(selectedTracks), + onPressed: () => showAddToPlaylistDialog(getSelectedTracks()), tooltip: lang.ADD_TO_PLAYLIST, icon: const Icon(Broken.music_playlist), ), IconButton( visualDensity: VisualDensity.compact, onPressed: () { - final tracks = selectedTracks; + final tracks = getSelectedTracks(); final selectedPl = SelectedTracksController.inst.selectedPlaylistsNames.values.toList(); selectedPl.removeDuplicates(); showGeneralPopupDialog( @@ -238,7 +241,7 @@ class SelectedTracksRow extends StatelessWidget { final maxLet = 20 - tracks.length.clamp(0, 17); return '${title.substring(0, (title.length > maxLet ? maxLet : title.length))}..'; }).join(', '), - tracksWithDates: SelectedTracksController.inst.selectedTracks.tracksWithDates.toList(), + tracksWithDates: SelectedTracksController.inst.selectedTracks.value.tracksWithDates.toList(), playlistName: selectedPl.length == 1 ? selectedPl.first : null, ); }, diff --git a/lib/ui/widgets/settings/advanced_settings.dart b/lib/ui/widgets/settings/advanced_settings.dart index e3eb51e7..b8eefb82 100644 --- a/lib/ui/widgets/settings/advanced_settings.dart +++ b/lib/ui/widgets/settings/advanced_settings.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:get/get.dart' hide Response; import 'package:namida/base/setting_subpage_provider.dart'; import 'package:namida/class/audio_cache_detail.dart'; @@ -22,6 +21,7 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/main.dart'; import 'package:namida/ui/dialogs/edit_tags_dialog.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; @@ -80,7 +80,7 @@ class AdvancedSettings extends SettingSubpageProvider { margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0), padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 6.0), borderRadius: 6.0, - bgColor: settings.performanceMode.value == e ? context.theme.cardColor : null, + bgColor: settings.performanceMode.valueR == e ? context.theme.cardColor : null, child: Row( children: [ Icon( @@ -90,7 +90,7 @@ class AdvancedSettings extends SettingSubpageProvider { const SizedBox(width: 6.0), Text( e.toText(), - style: context.textTheme.displayMedium?.copyWith(fontSize: 14.0.multipliedFontScale), + style: context.textTheme.displayMedium?.copyWith(fontSize: 14.0), ), ], ), @@ -105,8 +105,8 @@ class AdvancedSettings extends SettingSubpageProvider { ], child: Obx( () => Text( - settings.performanceMode.value.toText(), - style: context.textTheme.displaySmall?.copyWith(color: context.theme.colorScheme.onBackground.withAlpha(200)), + settings.performanceMode.valueR.toText(), + style: context.textTheme.displaySmall?.copyWith(color: context.theme.colorScheme.onSurface.withAlpha(200)), textAlign: TextAlign.end, ), ), @@ -135,8 +135,8 @@ class AdvancedSettings extends SettingSubpageProvider { ), trailingRaw: Obx( () { - final current = VideoController.inst.localVideoExtractCurrent.value; - final total = VideoController.inst.localVideoExtractTotal.value; + final current = VideoController.inst.localVideoExtractCurrent.valueR; + final total = VideoController.inst.localVideoExtractTotal.valueR; final isCounterVisible = total != 0; final isLoadingVisible = current != null; @@ -173,14 +173,12 @@ class AdvancedSettings extends SettingSubpageProvider { final RxMap sourcesMap = {}.obs; void resetSourcesMap() { - TrackSource.values.loop((e, index) { - sourcesMap[e] = 0; - }); + sourcesMap.execute((map) => TrackSource.values.loop((e) => map[e] = 0)); } - final RxInt totalTracksToBeRemoved = 0.obs; + final totalTracksToBeRemoved = 0.obs; - final RxInt totalTracksBetweenDates = 0.obs; + final totalTracksBetweenDates = 0.obs; void calculateTotalTracks(DateTime? oldest, DateTime? newest) { final sussyDays = HistoryController.inst.historyDays.toList(); @@ -193,9 +191,9 @@ class AdvancedSettings extends SettingSubpageProvider { printy(sussyDays); } resetSourcesMap(); - sussyDays.loop((d, index) { + sussyDays.loop((d) { final tracks = HistoryController.inst.historyMap.value[d] ?? []; - tracks.loop((twd, index) { + tracks.loop((twd) { sourcesMap.update(twd.source, (value) => value + 1, ifAbsent: () => 1); }); }); @@ -204,7 +202,7 @@ class AdvancedSettings extends SettingSubpageProvider { } if (sourcesToDelete.isNotEmpty) { totalTracksToBeRemoved.value = 0; - sourcesToDelete.loop((e, index) { + sourcesToDelete.loop((e) { totalTracksToBeRemoved.value += sourcesMap[e] ?? 0; }); } @@ -231,7 +229,7 @@ class AdvancedSettings extends SettingSubpageProvider { text: lang.REMOVE, onPressed: () async { final removedNum = await HistoryController.inst.removeSourcesTracksFromHistory( - sourcesToDelete, + sourcesToDelete.value, oldestDate: oldestDate, newestDate: newestDate, ); @@ -251,7 +249,7 @@ class AdvancedSettings extends SettingSubpageProvider { const Icon(Broken.danger), const SizedBox(width: 8.0), Obx(() => Text( - '${lang.TOTAL_TRACKS}: ${totalTracksToBeRemoved.value}', + '${lang.TOTAL_TRACKS}: ${totalTracksToBeRemoved.valueR}', style: context.textTheme.displayMedium, )), ], @@ -282,15 +280,18 @@ class AdvancedSettings extends SettingSubpageProvider { }, ), const SizedBox(height: 12.0), - BetweenDatesTextButton( - useHistoryDates: true, - onConfirm: (dates) { - oldestDate = dates.firstOrNull; - newestDate = dates.lastOrNull; - calculateTotalTracks(oldestDate, newestDate); - NamidaNavigator.inst.closeDialog(); - }, - tracksLength: totalTracksBetweenDates.value, + ObxO( + rx: totalTracksBetweenDates, + builder: (total) => BetweenDatesTextButton( + useHistoryDates: true, + onConfirm: (dates) { + oldestDate = dates.firstOrNull; + newestDate = dates.lastOrNull; + calculateTotalTracks(oldestDate, newestDate); + NamidaNavigator.inst.closeDialog(); + }, + tracksLength: total, + ), ), ], ), @@ -334,15 +335,18 @@ class AdvancedSettings extends SettingSubpageProvider { ), title: lang.MAX_IMAGE_CACHE_SIZE, trailing: Obx( - () => NamidaWheelSlider( - totalCount: getValue(4 * 1024), // 4 GB - initValue: getValue(settings.imagesMaxCacheInMB.value), - - text: (settings.imagesMaxCacheInMB.value * 1024 * 1024).fileSizeFormatted, - onValueChanged: (val) { - settings.save(imagesMaxCacheInMB: minimumValue + (val * stepper)); - }, - ), + () { + final maxInSettings = settings.imagesMaxCacheInMB.valueR; + return NamidaWheelSlider( + totalCount: getValue(4 * 1024), // 4 GB + initValue: getValue(maxInSettings), + + text: (maxInSettings * 1024 * 1024).fileSizeFormatted, + onValueChanged: (val) { + settings.save(imagesMaxCacheInMB: minimumValue + (val * stepper)); + }, + ); + }, ), ), ); @@ -362,15 +366,17 @@ class AdvancedSettings extends SettingSubpageProvider { ), title: lang.MAX_AUDIO_CACHE_SIZE, trailing: Obx( - () => NamidaWheelSlider( - totalCount: getValue(4 * 1024), // 4 GB - initValue: getValue(settings.audiosMaxCacheInMB.value), - - text: (settings.audiosMaxCacheInMB.value * 1024 * 1024).fileSizeFormatted, - onValueChanged: (val) { - settings.save(audiosMaxCacheInMB: minimumValue + (val * stepper)); - }, - ), + () { + final maxInSettings = settings.audiosMaxCacheInMB.valueR; + return NamidaWheelSlider( + totalCount: getValue(4 * 1024), // 4 GB + initValue: getValue(maxInSettings), + text: (maxInSettings * 1024 * 1024).fileSizeFormatted, + onValueChanged: (val) { + settings.save(audiosMaxCacheInMB: minimumValue + (val * stepper)); + }, + ); + }, ), ), ); @@ -390,15 +396,17 @@ class AdvancedSettings extends SettingSubpageProvider { ), title: lang.MAX_VIDEO_CACHE_SIZE, trailing: Obx( - () => NamidaWheelSlider( - totalCount: getValue(10 * 1024), // 10 GB - initValue: getValue(settings.videosMaxCacheInMB.value), - - text: (settings.videosMaxCacheInMB.value * 1024 * 1024).fileSizeFormatted, - onValueChanged: (val) { - settings.save(videosMaxCacheInMB: minimumValue + (val * stepper)); - }, - ), + () { + final maxInSettings = settings.videosMaxCacheInMB.valueR; + return NamidaWheelSlider( + totalCount: getValue(10 * 1024), // 10 GB + initValue: getValue(maxInSettings), + text: (maxInSettings * 1024 * 1024).fileSizeFormatted, + onValueChanged: (val) { + settings.save(videosMaxCacheInMB: minimumValue + (val * stepper)); + }, + ); + }, ), ), ); @@ -427,7 +435,7 @@ class AdvancedSettings extends SettingSubpageProvider { secondaryIcon: Broken.close_circle, ), title: lang.CLEAR_VIDEO_CACHE, - trailingText: Indexer.inst.videosSizeInStorage.value.fileSizeFormatted, + trailingText: Indexer.inst.videosSizeInStorage.valueR.fileSizeFormatted, onTap: () { final allvideos = VideoController.inst.getCurrentVideosInCache(); const cacheManager = StorageCacheManager(); @@ -535,13 +543,13 @@ class __ClearImageCacheListTileState extends State<_ClearImageCacheListTile> { const CancelButton(), Obx( () { - final total = dirsChoosen.fold(0, (p, element) => p + (dirsMap[element] ?? 0)); + final total = dirsChoosen.valueR.fold(0, (p, element) => p + (dirsMap[element] ?? 0)); return NamidaButton( text: "${lang.CLEAR.toUpperCase()} (${total.fileSizeFormatted})", onPressed: () async { NamidaNavigator.inst.closeDialog(); - for (final d in dirsChoosen) { + for (final d in dirsChoosen.value) { await Directory(d).delete(recursive: true); await Directory(d).create(); } @@ -612,7 +620,7 @@ class __ClearAudioCacheListTileState extends State<_ClearAudioCacheListTile> { static int _fillSizeIsolate(String dirPath) { int size = 0; - Directory(dirPath).listSyncSafe().loop((e, _) { + Directory(dirPath).listSyncSafe().loop((e) { size += e.statSync().size; }); return size; @@ -620,14 +628,14 @@ class __ClearAudioCacheListTileState extends State<_ClearAudioCacheListTile> { static int _tempFilesSizeIsolate(String dirPath) { int size = 0; - Directory(dirPath).listSyncSafe().loop((e, _) { + Directory(dirPath).listSyncSafe().loop((e) { if (e.path.endsWith('.part')) size += e.statSync().size; }); return size; } static void _tempFilesDeleteIsolate(String dirPath) { - Directory(dirPath).listSyncSafe().loop((e, _) { + Directory(dirPath).listSyncSafe().loop((e) { if (e.path.endsWith('.part')) { if (e is File) { try { @@ -651,7 +659,7 @@ class __ClearAudioCacheListTileState extends State<_ClearAudioCacheListTile> { onTap: () { final allaudios = []; for (final acFiles in Player.inst.audioCacheMap.values) { - acFiles.loop((e, index) => allaudios.add(e)); + acFiles.loop((e) => allaudios.add(e)); } const cacheManager = StorageCacheManager(); @@ -829,8 +837,8 @@ class UpdateDirectoryPathListTile extends StatelessWidget { () => CustomSwitchListTile( passedColor: colorScheme, title: lang.UPDATE_MISSING_TRACKS_ONLY, - value: updateMissingOnly.value, - onChanged: (isTrue) => updateMissingOnly.value = !updateMissingOnly.value, + value: updateMissingOnly.valueR, + onChanged: (isTrue) => updateMissingOnly.toggle(), ), ), ], @@ -909,7 +917,7 @@ class _CompressImagesListTile extends StatelessWidget { text: lang.COMPRESS, onPressed: () { NamidaNavigator.inst.closeDialog(); - _startCompressing(dirsToCompress, compPerc.value, keepOriginalFileDates.value); + _startCompressing(dirsToCompress.value, compPerc.value, keepOriginalFileDates.value); }, ), ], @@ -920,7 +928,7 @@ class _CompressImagesListTile extends StatelessWidget { padding: EdgeInsets.zero, shrinkWrap: true, children: [ - ...initialDirectories.map( + ...initialDirectories.valueR.map( (e) => Obx( () { final dirPath = e.split(Platform.pathSeparator)..removeWhere((element) => element == ''); @@ -947,7 +955,7 @@ class _CompressImagesListTile extends StatelessWidget { () => NamidaWheelSlider( totalCount: 100, initValue: 50, - text: "${compPerc.value}%", + text: "${compPerc.valueR}%", onValueChanged: (val) { compPerc.value = val; }, @@ -968,7 +976,7 @@ class _CompressImagesListTile extends StatelessWidget { () => CustomSwitchListTile( icon: Broken.document_code_2, title: lang.KEEP_FILE_DATES, - value: keepOriginalFileDates.value, + value: keepOriginalFileDates.valueR, onChanged: (isTrue) => keepOriginalFileDates.value = !isTrue, ), ), diff --git a/lib/ui/widgets/settings/backup_restore_settings.dart b/lib/ui/widgets/settings/backup_restore_settings.dart index e4a3424e..8a156087 100644 --- a/lib/ui/widgets/settings/backup_restore_settings.dart +++ b/lib/ui/widgets/settings/backup_restore_settings.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/base/setting_subpage_provider.dart'; import 'package:namida/controller/backup_controller.dart'; @@ -15,6 +14,7 @@ import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/main.dart'; import 'package:namida/ui/widgets/circular_percentages.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; @@ -128,7 +128,7 @@ class BackupAndRestore extends SettingSubpageProvider { bgColor: getBgColor(_BackupAndRestoreKeys.defaultLocation), title: lang.DEFAULT_BACKUP_LOCATION, icon: Broken.direct_inbox, - subtitle: settings.defaultBackupLocation.value, + subtitle: settings.defaultBackupLocation.valueR, onTap: () async { final path = await NamidaFileBrowser.getDirectory(note: lang.DEFAULT_BACKUP_LOCATION); @@ -230,7 +230,7 @@ class BackupAndRestore extends SettingSubpageProvider { (int, bool) getItemsSize(List items, Map map) { int s = 0; bool hasUnknown = false; - items.loop((e, _) { + items.loop((e) { if (map[e] == null) { hasUnknown = true; } else { @@ -250,8 +250,8 @@ class BackupAndRestore extends SettingSubpageProvider { }) { return Obx( () { - final localRes = getItemsSize(items, sizesMap); - final ytRes = getItemsSize(youtubeItems, sizesMap); + final localRes = getItemsSize(items, sizesMap.valueR); + final ytRes = getItemsSize(youtubeItems, sizesMap.valueR); final localSize = localRes.$1; final ytSize = ytRes.$1; final localUnknown = localRes.$2; @@ -347,13 +347,13 @@ class BackupAndRestore extends SettingSubpageProvider { onPressed: () { if (settings.backupItemslist.isNotEmpty) { NamidaNavigator.inst.closeDialog(); - BackupController.inst.createBackupFile(settings.backupItemslist); + BackupController.inst.createBackupFile(settings.backupItemslist.value); } }, ), ], child: SizedBox( - height: Get.height / 2, + height: namida.height / 2, child: SingleChildScrollView( child: Column( children: [ @@ -526,7 +526,7 @@ class BackupAndRestore extends SettingSubpageProvider { icon: Broken.timer, trailing: Obx( () { - final days = settings.autoBackupIntervalDays.value; + final days = settings.autoBackupIntervalDays.valueR; return NamidaWheelSlider( totalCount: 14, initValue: days, @@ -580,16 +580,16 @@ class BackupAndRestore extends SettingSubpageProvider { Widget getTitleText(String text) => Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0).add(const EdgeInsets.only(bottom: 10.0)), - child: Text("- $text", style: Get.textTheme.displayLarge), + child: Text("- $text", style: namida.textTheme.displayLarge), ); final jsonfile = await NamidaFileBrowser.pickFile(note: lang.IMPORT_YOUTUBE_HISTORY, allowedExtensions: ['json', 'JSON']); if (jsonfile != null) { - final RxBool isMatchingTypeLink = true.obs; - final RxBool isMatchingTypeTitleAndArtist = false.obs; - final RxBool matchYT = true.obs; - final RxBool matchYTMusic = true.obs; - final RxBool matchAll = false.obs; + final isMatchingTypeLink = true.obs; + final isMatchingTypeTitleAndArtist = false.obs; + final matchYT = true.obs; + final matchYTMusic = true.obs; + final matchAll = false.obs; final oldestDate = Rxn(); DateTime? newestDate; NamidaNavigator.inst.navigateDialog( @@ -606,8 +606,8 @@ class BackupAndRestore extends SettingSubpageProvider { actions: [ Obx( () => NamidaButton( - enabled: isMatchingTypeLink.value || isMatchingTypeTitleAndArtist.value, - textWidget: Obx(() => Text(oldestDate.value != null ? lang.IMPORT_TIME_RANGE : lang.IMPORT_ALL)), + enabled: isMatchingTypeLink.valueR || isMatchingTypeTitleAndArtist.valueR, + textWidget: Obx(() => Text(oldestDate.valueR != null ? lang.IMPORT_TIME_RANGE : lang.IMPORT_ALL)), onPressed: () async { NamidaNavigator.inst.closeDialog(); await JsonToHistoryParser.inst.addFileSourceToNamidaHistory( @@ -629,44 +629,36 @@ class BackupAndRestore extends SettingSubpageProvider { crossAxisAlignment: CrossAxisAlignment.start, children: [ getTitleText(lang.SOURCE), - Obx( - () => ListTileWithCheckMark( - active: matchYT.value, - title: lang.YOUTUBE, - onTap: () => matchYT.value = !matchYT.value, - ), + ListTileWithCheckMark( + activeRx: matchYT, + title: lang.YOUTUBE, + onTap: matchYT.toggle, ), const SizedBox(height: 8.0), - Obx( - () => ListTileWithCheckMark( - active: matchYTMusic.value, - title: lang.YOUTUBE_MUSIC, - onTap: () => matchYTMusic.value = !matchYTMusic.value, - ), + ListTileWithCheckMark( + activeRx: matchYTMusic, + title: lang.YOUTUBE_MUSIC, + onTap: matchYTMusic.toggle, ), getDivider(), getTitleText(lang.MATCHING_TYPE), - Obx( - () => ListTileWithCheckMark( - active: isMatchingTypeLink.value, - title: lang.LINK, - onTap: () => isMatchingTypeLink.value = !isMatchingTypeLink.value, - ), + ListTileWithCheckMark( + activeRx: isMatchingTypeLink, + title: lang.LINK, + onTap: isMatchingTypeLink.toggle, ), const SizedBox(height: 8.0), - Obx( - () => ListTileWithCheckMark( - active: isMatchingTypeTitleAndArtist.value, - title: [lang.TITLE, lang.ARTIST].join(' & '), - onTap: () => isMatchingTypeTitleAndArtist.value = !isMatchingTypeTitleAndArtist.value, - ), + ListTileWithCheckMark( + activeRx: isMatchingTypeTitleAndArtist, + title: [lang.TITLE, lang.ARTIST].join(' & '), + onTap: isMatchingTypeTitleAndArtist.toggle, ), getDivider(), Obx( () => matchAllTracksListTile( - active: matchAll.value, - onTap: () => matchAll.value = !matchAll.value, - displayPerfWarning: isMatchingTypeTitleAndArtist.value, // link matching wont result in perf issue + active: matchAll.valueR, + onTap: matchAll.toggle, + displayPerfWarning: isMatchingTypeTitleAndArtist.valueR, // link matching wont result in perf issue ), ), getDivider(), @@ -751,7 +743,7 @@ class BackupAndRestore extends SettingSubpageProvider { actions: [ const CancelButton(), NamidaButton( - textWidget: Obx(() => Text(oldestDate.value != null ? lang.IMPORT_TIME_RANGE : lang.IMPORT_ALL)), + textWidget: Obx(() => Text(oldestDate.valueR != null ? lang.IMPORT_TIME_RANGE : lang.IMPORT_ALL)), onPressed: () async { NamidaNavigator.inst.closeDialog(); await JsonToHistoryParser.inst.addFileSourceToNamidaHistory( @@ -769,8 +761,8 @@ class BackupAndRestore extends SettingSubpageProvider { children: [ Obx( () => matchAllTracksListTile( - active: matchAll.value, - onTap: () => matchAll.value = !matchAll.value, + active: matchAll.valueR, + onTap: matchAll.toggle, displayPerfWarning: true, ), ), diff --git a/lib/ui/widgets/settings/customization_settings.dart b/lib/ui/widgets/settings/customization_settings.dart index ef021917..1654f117 100644 --- a/lib/ui/widgets/settings/customization_settings.dart +++ b/lib/ui/widgets/settings/customization_settings.dart @@ -1,7 +1,6 @@ // ignore_for_file: constant_identifier_names import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/base/setting_subpage_provider.dart'; import 'package:namida/class/track.dart'; @@ -16,6 +15,7 @@ import 'package:namida/core/functions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/dialogs/setting_dialog_with_text_field.dart'; import 'package:namida/ui/widgets/artwork.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; @@ -129,7 +129,7 @@ class CustomizationSettings extends SettingSubpageProvider { performanceMode: PerformanceMode.custom, ); }, - value: settings.enableBlurEffect.value, + value: settings.enableBlurEffect.valueR, ), ), ), @@ -147,7 +147,7 @@ class CustomizationSettings extends SettingSubpageProvider { performanceMode: PerformanceMode.custom, ); }, - value: settings.enableGlowEffect.value, + value: settings.enableGlowEffect.valueR, ), ), ), @@ -163,7 +163,7 @@ class CustomizationSettings extends SettingSubpageProvider { enableMiniplayerParallaxEffect: !isTrue, performanceMode: PerformanceMode.custom, ), - value: settings.enableMiniplayerParallaxEffect.value, + value: settings.enableMiniplayerParallaxEffect.valueR, ), ), ), @@ -175,7 +175,7 @@ class CustomizationSettings extends SettingSubpageProvider { icon: Broken.timer, title: lang.DISPLAY_REMAINING_DURATION_INSTEAD_OF_TOTAL, onChanged: (isTrue) => settings.player.save(displayRemainingDurInsteadOfTotal: !isTrue), - value: settings.player.displayRemainingDurInsteadOfTotal.value, + value: settings.player.displayRemainingDurInsteadOfTotal.valueR, ), ), ), @@ -186,7 +186,7 @@ class CustomizationSettings extends SettingSubpageProvider { bgColor: getBgColor(_CustomizationSettingsKeys.brMultiplier), icon: Broken.rotate_left_1, title: lang.BORDER_RADIUS_MULTIPLIER, - trailingText: "${settings.borderRadiusMultiplier.value}", + trailingText: "${settings.borderRadiusMultiplier.valueR}", onTap: () { showSettingDialogWithTextField( title: lang.BORDER_RADIUS_MULTIPLIER, @@ -204,7 +204,7 @@ class CustomizationSettings extends SettingSubpageProvider { bgColor: getBgColor(_CustomizationSettingsKeys.fontScale), icon: Broken.text, title: lang.FONT_SCALE, - trailingText: "${(settings.fontScaleFactor.value * 100).toInt()}%", + trailingText: "${(settings.fontScaleFactor.valueR * 100).toInt()}%", onTap: () { showSettingDialogWithTextField( title: lang.FONT_SCALE, @@ -225,7 +225,7 @@ class CustomizationSettings extends SettingSubpageProvider { onChanged: (p0) { settings.save(hourFormat12: !p0); }, - value: settings.hourFormat12.value, + value: settings.hourFormat12.valueR, ), ), ), @@ -236,7 +236,7 @@ class CustomizationSettings extends SettingSubpageProvider { bgColor: getBgColor(_CustomizationSettingsKeys.dateTimeFormat), icon: Broken.calendar_edit, title: lang.DATE_TIME_FORMAT, - trailingText: "${settings.dateTimeFormat}", + trailingText: settings.dateTimeFormat.valueR, onTap: () async { final scrollController = ScrollController(); @@ -245,7 +245,7 @@ class CustomizationSettings extends SettingSubpageProvider { icon: Broken.calendar_edit, dateTimeFormat: true, topWidget: SizedBox( - height: Get.height * 0.4, + height: namida.height * 0.4, child: Padding( padding: const EdgeInsets.only(bottom: 58.0), child: Stack( @@ -273,7 +273,7 @@ class CustomizationSettings extends SettingSubpageProvider { right: 0, child: Container( padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration(color: Get.theme.cardTheme.color, shape: BoxShape.circle), + decoration: BoxDecoration(color: namida.theme.cardTheme.color, shape: BoxShape.circle), child: NamidaIconButton( icon: Broken.arrow_circle_down, onPressed: () { @@ -325,7 +325,7 @@ class CustomizationSettings extends SettingSubpageProvider { icon: Broken.card_remove, title: lang.DISPLAY_TRACK_NUMBER_IN_ALBUM_PAGE, subtitle: lang.DISPLAY_TRACK_NUMBER_IN_ALBUM_PAGE_SUBTITLE, - value: settings.displayTrackNumberinAlbumPage.value, + value: settings.displayTrackNumberinAlbumPage.valueR, onChanged: (p0) => settings.save(displayTrackNumberinAlbumPage: !p0), ), ), @@ -341,7 +341,7 @@ class CustomizationSettings extends SettingSubpageProvider { title: lang.DISPLAY_ALBUM_CARD_TOP_RIGHT_DATE, subtitle: lang.DISPLAY_ALBUM_CARD_TOP_RIGHT_DATE_SUBTITLE, onChanged: (p0) => settings.save(albumCardTopRightDate: !p0), - value: settings.albumCardTopRightDate.value, + value: settings.albumCardTopRightDate.valueR, ), ), ), @@ -354,10 +354,10 @@ class CustomizationSettings extends SettingSubpageProvider { bgColor: getBgColor(_CustomizationSettingsKeys.forceSquaredAlbumThumb), icon: Broken.crop, title: lang.FORCE_SQUARED_ALBUM_THUMBNAIL, - value: settings.forceSquaredAlbumThumbnail.value, + value: settings.forceSquaredAlbumThumbnail.valueR, onChanged: (p0) { settings.save(forceSquaredAlbumThumbnail: !p0); - if (!p0 && settings.albumThumbnailSizeinList.toInt() != settings.albumListTileHeight.toInt()) { + if (!p0 && settings.albumThumbnailSizeinList.value.toInt() != settings.albumListTileHeight.value.toInt()) { NamidaNavigator.inst.navigateDialog( dialog: CustomBlurryDialog( normalTitleStyle: true, @@ -389,7 +389,7 @@ class CustomizationSettings extends SettingSubpageProvider { bgColor: getBgColor(_CustomizationSettingsKeys.staggeredAlbumGridview), icon: Broken.element_4, title: lang.STAGGERED_ALBUM_GRID_VIEW, - value: settings.useAlbumStaggeredGridView.value, + value: settings.useAlbumStaggeredGridView.valueR, onChanged: (p0) => settings.save(useAlbumStaggeredGridView: !p0), ), ), @@ -403,7 +403,7 @@ class CustomizationSettings extends SettingSubpageProvider { bgColor: getBgColor(_CustomizationSettingsKeys.sizeOfAlbumThumb), icon: Broken.maximize_3, title: lang.ALBUM_THUMBNAIL_SIZE_IN_LIST, - trailingText: "${settings.albumThumbnailSizeinList.toInt()}", + trailingText: "${settings.albumThumbnailSizeinList.valueR.toInt()}", onTap: () { showSettingDialogWithTextField( title: lang.ALBUM_THUMBNAIL_SIZE_IN_LIST, @@ -423,7 +423,7 @@ class CustomizationSettings extends SettingSubpageProvider { bgColor: getBgColor(_CustomizationSettingsKeys.heightOfAlbumTile), icon: Broken.pharagraphspacing, title: lang.HEIGHT_OF_ALBUM_TILE, - trailingText: "${settings.albumListTileHeight.toInt()}", + trailingText: "${settings.albumListTileHeight.valueR.toInt()}", onTap: () { showSettingDialogWithTextField( title: lang.HEIGHT_OF_ALBUM_TILE, @@ -448,8 +448,8 @@ class CustomizationSettings extends SettingSubpageProvider { normalTitleStyle: true, insetPadding: const EdgeInsets.all(64.0), child: SizedBox( - height: Get.height * 0.5, - width: Get.width, + height: namida.height * 0.5, + width: namida.width, child: NamidaListView( padding: EdgeInsets.zero, itemBuilder: (context, i) { @@ -466,7 +466,7 @@ class CustomizationSettings extends SettingSubpageProvider { ); }, itemCount: TrackTileItem.values.length, - itemExtents: null, + itemExtent: null, ), ), ), @@ -492,12 +492,12 @@ class CustomizationSettings extends SettingSubpageProvider { bgColor: getBgColor(_CustomizationSettingsKeys.forceSquaredTrackThumb), icon: Broken.crop, title: lang.FORCE_SQUARED_TRACK_THUMBNAIL, - value: settings.forceSquaredTrackThumbnail.value, + value: settings.forceSquaredTrackThumbnail.valueR, onChanged: (value) { settings.save(forceSquaredTrackThumbnail: !value); Player.inst.refreshRxVariables(); _onSettingsChanged(); - if (!value && settings.trackThumbnailSizeinList.toInt() != settings.trackListTileHeight.toInt()) { + if (!value && settings.trackThumbnailSizeinList.value.toInt() != settings.trackListTileHeight.value.toInt()) { NamidaNavigator.inst.navigateDialog( dialog: CustomBlurryDialog( normalTitleStyle: true, @@ -527,7 +527,7 @@ class CustomizationSettings extends SettingSubpageProvider { bgColor: getBgColor(_CustomizationSettingsKeys.sizeOfTrackThumb), icon: Broken.maximize_3, title: lang.TRACK_THUMBNAIL_SIZE_IN_LIST, - trailingText: "${settings.trackThumbnailSizeinList.toInt()}", + trailingText: "${settings.trackThumbnailSizeinList.valueR.toInt()}", onTap: () { showSettingDialogWithTextField( title: lang.TRACK_THUMBNAIL_SIZE_IN_LIST, @@ -545,7 +545,7 @@ class CustomizationSettings extends SettingSubpageProvider { bgColor: getBgColor(_CustomizationSettingsKeys.heightOfTrackTile), icon: Broken.pharagraphspacing, title: lang.HEIGHT_OF_TRACK_TILE, - trailingText: "${settings.trackListTileHeight.toInt()}", + trailingText: "${settings.trackListTileHeight.valueR.toInt()}", onTap: () { showSettingDialogWithTextField( title: lang.HEIGHT_OF_TRACK_TILE, @@ -568,7 +568,7 @@ class CustomizationSettings extends SettingSubpageProvider { settings.save(displayThirdRow: !isTrue); _onSettingsChanged(); }, - value: settings.displayThirdRow.value, + value: settings.displayThirdRow.valueR, ), ), ), @@ -584,7 +584,7 @@ class CustomizationSettings extends SettingSubpageProvider { settings.save(displayThirdItemInEachRow: !isTrue); _onSettingsChanged(); }, - value: settings.displayThirdItemInEachRow.value, + value: settings.displayThirdItemInEachRow.valueR, ), ), ), @@ -599,7 +599,7 @@ class CustomizationSettings extends SettingSubpageProvider { settings.save(displayFavouriteIconInListTile: !isTrue); _onSettingsChanged(); }, - value: settings.displayFavouriteIconInListTile.value, + value: settings.displayFavouriteIconInListTile.valueR, ), ), ), @@ -610,7 +610,7 @@ class CustomizationSettings extends SettingSubpageProvider { bgColor: getBgColor(_CustomizationSettingsKeys.itemsSeparator), icon: Broken.minus_square, title: lang.TRACK_TILE_ITEMS_SEPARATOR, - trailingText: settings.trackTileSeparator.value, + trailingText: settings.trackTileSeparator.valueR, onTap: () => showSettingDialogWithTextField( title: lang.TRACK_TILE_ITEMS_SEPARATOR, trackTileSeparator: true, @@ -623,7 +623,7 @@ class CustomizationSettings extends SettingSubpageProvider { () => Container( color: context.theme.cardTheme.color, width: context.width, - height: settings.trackListTileHeight * 1.5, + height: settings.trackListTileHeight.valueR * 1.5, alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 7.0), child: Row( @@ -637,14 +637,14 @@ class CustomizationSettings extends SettingSubpageProvider { margin: const EdgeInsets.symmetric( horizontal: 0.0, ), - width: settings.trackThumbnailSizeinList.value, - height: settings.trackThumbnailSizeinList.value, + width: settings.trackThumbnailSizeinList.valueR, + height: settings.trackThumbnailSizeinList.valueR, child: ArtworkWidget( track: allTracksInLibrary.firstOrNull, key: Key(allTracksInLibrary.firstOrNull?.pathToImage ?? ''), - thumbnailSize: settings.trackThumbnailSizeinList.value, + thumbnailSize: settings.trackThumbnailSizeinList.valueR, path: allTracksInLibrary.firstOrNull?.pathToImage, - forceSquared: settings.forceSquaredTrackThumbnail.value, + forceSquared: settings.forceSquaredTrackThumbnail.valueR, ), ), const SizedBox( @@ -662,7 +662,7 @@ class CustomizationSettings extends SettingSubpageProvider { children: [ TrackTilePosition.row1Item1, TrackTilePosition.row1Item2, - if (settings.displayThirdItemInEachRow.value) TrackTilePosition.row1Item3, + if (settings.displayThirdItemInEachRow.valueR) TrackTilePosition.row1Item3, ] .map( (e) => TrackItemSmallBox( @@ -682,7 +682,7 @@ class CustomizationSettings extends SettingSubpageProvider { children: [ TrackTilePosition.row2Item1, TrackTilePosition.row2Item2, - if (settings.displayThirdItemInEachRow.value) TrackTilePosition.row2Item3, + if (settings.displayThirdItemInEachRow.valueR) TrackTilePosition.row2Item3, ] .map( (e) => TrackItemSmallBox( @@ -697,13 +697,13 @@ class CustomizationSettings extends SettingSubpageProvider { const SizedBox( height: 4.0, ), - if (settings.displayThirdRow.value) + if (settings.displayThirdRow.valueR) FittedBox( child: Row( children: [ TrackTilePosition.row3Item1, TrackTilePosition.row3Item2, - if (settings.displayThirdItemInEachRow.value) TrackTilePosition.row3Item3, + if (settings.displayThirdItemInEachRow.valueR) TrackTilePosition.row3Item3, ] .map( (e) => TrackItemSmallBox( @@ -735,9 +735,8 @@ class CustomizationSettings extends SettingSubpageProvider { onTap: () => _showTrackItemsDialog(e), ), ) - .addSeparators(separator: const SizedBox(height: 3.0)) - .toList(), - if (settings.displayFavouriteIconInListTile.value) ...[ + .addSeparators(separator: const SizedBox(height: 3.0)), + if (settings.displayFavouriteIconInListTile.valueR) ...[ const SizedBox(height: 3.0), const NamidaLikeButton( track: null, @@ -784,7 +783,7 @@ class CustomizationSettings extends SettingSubpageProvider { if (value) return settings.save(enablePartyModeInMiniplayer: false); SussyBaka.monetize(onEnable: () => settings.save(enablePartyModeInMiniplayer: true)); }, - value: settings.enablePartyModeInMiniplayer.value, + value: settings.enablePartyModeInMiniplayer.valueR, ), ), ), @@ -793,13 +792,13 @@ class CustomizationSettings extends SettingSubpageProvider { child: Obx( () => CustomSwitchListTile( bgColor: getBgColor(_CustomizationSettingsKeys.edgeColorsSwitching), - enabled: settings.enablePartyModeInMiniplayer.value, + enabled: settings.enablePartyModeInMiniplayer.valueR, icon: Broken.colors_square, title: lang.EDGE_COLORS_SWITCHING, onChanged: (value) { settings.save(enablePartyModeColorSwap: !value); }, - value: settings.enablePartyModeColorSwap.value, + value: settings.enablePartyModeColorSwap.valueR, ), ), ), @@ -811,7 +810,7 @@ class CustomizationSettings extends SettingSubpageProvider { icon: Broken.buy_crypto, title: lang.ENABLE_MINIPLAYER_PARTICLES, onChanged: (value) => settings.save(enableMiniplayerParticles: !value), - value: settings.enableMiniplayerParticles.value, + value: settings.enableMiniplayerParticles.valueR, ), ), ), @@ -824,11 +823,11 @@ class CustomizationSettings extends SettingSubpageProvider { title: lang.ANIMATING_THUMBNAIL_INTENSITY, trailing: NamidaWheelSlider( totalCount: 25, - initValue: settings.animatingThumbnailIntensity.value, + initValue: settings.animatingThumbnailIntensity.valueR, onValueChanged: (val) { settings.save(animatingThumbnailIntensity: val); }, - text: "${(settings.animatingThumbnailIntensity.value * 4).toStringAsFixed(0)}%", + text: "${(settings.animatingThumbnailIntensity.valueR * 4).toStringAsFixed(0)}%", ), ), ), @@ -844,7 +843,7 @@ class CustomizationSettings extends SettingSubpageProvider { onChanged: (value) { settings.save(animatingThumbnailInversed: !value); }, - value: settings.animatingThumbnailInversed.value, + value: settings.animatingThumbnailInversed.valueR, ), ), ), @@ -882,8 +881,8 @@ class CustomizationSettings extends SettingSubpageProvider { visualDensity: VisualDensity.compact, icon: Broken.maximize, title: lang.SCALE_MULTIPLIER, - subtitle: "${(settings.animatingThumbnailScaleMultiplier.value * 100).round()}%", - value: settings.artworkGestureScale.value, + subtitle: "${(settings.animatingThumbnailScaleMultiplier.valueR * 100).round()}%", + value: settings.artworkGestureScale.valueR, onChanged: (value) { settings.save(artworkGestureScale: !value); }, @@ -898,7 +897,7 @@ class CustomizationSettings extends SettingSubpageProvider { secondaryIconSize: 12.0, ), title: lang.DOUBLE_TAP_TO_TOGGLE_LYRICS, - value: settings.artworkGestureDoubleTapLRC.value, + value: settings.artworkGestureDoubleTapLRC.valueR, onChanged: (value) { settings.save(artworkGestureDoubleTapLRC: !value); }, @@ -922,13 +921,13 @@ class CustomizationSettings extends SettingSubpageProvider { children: [ NamidaWheelSlider( totalCount: 360, - initValue: settings.waveformTotalBars.value - 40, + initValue: settings.waveformTotalBars.valueR - 40, onValueChanged: (val) { final v = (val + 40); settings.save(waveformTotalBars: v); WaveformController.inst.calculateUIWaveform(); }, - text: settings.waveformTotalBars.value.toString(), + text: settings.waveformTotalBars.valueR.toString(), ), ], ), @@ -944,7 +943,7 @@ class CustomizationSettings extends SettingSubpageProvider { icon: Broken.text_block, title: lang.DISPLAY_AUDIO_INFO_IN_MINIPLAYER, onChanged: (value) => settings.save(displayAudioInfoMiniplayer: !value), - value: settings.displayAudioInfoMiniplayer.value, + value: settings.displayAudioInfoMiniplayer.valueR, ), ), ), @@ -959,7 +958,7 @@ class CustomizationSettings extends SettingSubpageProvider { settings.save(displayArtistBeforeTitle: !value); Player.inst.refreshRxVariables(); }, - value: settings.displayArtistBeforeTitle.value, + value: settings.displayArtistBeforeTitle.valueR, ), ), ), @@ -978,7 +977,7 @@ class TrackItemSmallBox extends StatelessWidget { @override Widget build(BuildContext context) { return NamidaInkWell( - bgColor: context.theme.colorScheme.background.withAlpha(160), + bgColor: context.theme.colorScheme.surface.withAlpha(160), onTap: onTap, borderRadius: 8.0, padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), diff --git a/lib/ui/widgets/settings/extra_settings.dart b/lib/ui/widgets/settings/extra_settings.dart index a7dad0f9..ccb817af 100644 --- a/lib/ui/widgets/settings/extra_settings.dart +++ b/lib/ui/widgets/settings/extra_settings.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - import 'package:namida/base/setting_subpage_provider.dart'; import 'package:namida/controller/current_color.dart'; import 'package:namida/controller/folders_controller.dart'; import 'package:namida/controller/indexer_controller.dart'; -import 'package:namida/controller/namida_channel.dart'; import 'package:namida/controller/miniplayer_controller.dart'; +import 'package:namida/controller/namida_channel.dart'; import 'package:namida/controller/navigator_controller.dart'; import 'package:namida/controller/settings_controller.dart'; import 'package:namida/core/constants.dart'; @@ -16,6 +14,7 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/settings_card.dart'; @@ -84,7 +83,7 @@ class ExtrasSettings extends SettingSubpageProvider { icon: Broken.direct, title: lang.ENABLE_BOTTOM_NAV_BAR, subtitle: lang.ENABLE_BOTTOM_NAV_BAR_SUBTITLE, - value: settings.enableBottomNavBar.value, + value: settings.enableBottomNavBar.valueR, onChanged: (p0) { settings.save(enableBottomNavBar: !p0); MiniPlayerController.inst.updateBottomNavBarRelatedDimensions(!p0); @@ -99,7 +98,7 @@ class ExtrasSettings extends SettingSubpageProvider { bgColor: getBgColor(_ExtraSettingsKeys.pip), icon: Broken.screenmirroring, title: lang.ENABLE_PICTURE_IN_PICTURE, - value: settings.enablePip.value, + value: settings.enablePip.valueR, onChanged: (isTrue) { settings.save(enablePip: !isTrue); NamidaChannel.inst.setCanEnterPip(!isTrue); @@ -114,7 +113,7 @@ class ExtrasSettings extends SettingSubpageProvider { bgColor: getBgColor(_ExtraSettingsKeys.foldersHierarchy), icon: Broken.folder_open, title: lang.ENABLE_FOLDERS_HIERARCHY, - value: settings.enableFoldersHierarchy.value, + value: settings.enableFoldersHierarchy.valueR, onChanged: (p0) { settings.save(enableFoldersHierarchy: !p0); Folders.inst.onFoldersHierarchyChanged(!p0); @@ -129,7 +128,7 @@ class ExtrasSettings extends SettingSubpageProvider { bgColor: getBgColor(_ExtraSettingsKeys.fabType), icon: Broken.safe_home, title: lang.FLOATING_ACTION_BUTTON, - trailingText: settings.floatingActionButton.value.toText(), + trailingText: settings.floatingActionButton.valueR.toText(), onTap: () { NamidaNavigator.inst.navigateDialog( dialog: CustomBlurryDialog( @@ -145,13 +144,14 @@ class ExtrasSettings extends SettingSubpageProvider { child: Column( children: FABType.values .map( - (e) => Obx( - () => Container( + (e) => ObxO( + rx: settings.floatingActionButton, + builder: (floatingActionButton) => Container( margin: const EdgeInsets.all(4.0), child: ListTileWithCheckMark( title: e.toText(), icon: e.toIcon(), - active: settings.floatingActionButton.value == e, + active: floatingActionButton == e, onTap: () => settings.save(floatingActionButton: e), ), ), @@ -173,7 +173,7 @@ class ExtrasSettings extends SettingSubpageProvider { bgColor: getBgColor(_ExtraSettingsKeys.defaultLibraryTab), icon: Broken.receipt_1, title: lang.DEFAULT_LIBRARY_TAB, - trailingText: settings.autoLibraryTab.value ? lang.AUTO : settings.staticLibraryTab.value.toText(), + trailingText: settings.autoLibraryTab.valueR ? lang.AUTO : settings.staticLibraryTab.valueR.toText(), onTap: () => NamidaNavigator.inst.navigateDialog( dialog: CustomBlurryDialog( title: lang.DEFAULT_LIBRARY_TAB, @@ -194,12 +194,12 @@ class ExtrasSettings extends SettingSubpageProvider { title: lang.AUTO, icon: Broken.recovery_convert, onTap: () => settings.save(autoLibraryTab: true), - active: settings.autoLibraryTab.value, + active: settings.autoLibraryTab.valueR, ), ), ), const SizedBox(height: 12.0), - ...settings.libraryTabs.asMap().entries.map( + ...settings.libraryTabs.value.asMap().entries.map( (e) => Obx( () => Container( margin: const EdgeInsets.all(4.0), @@ -213,7 +213,7 @@ class ExtrasSettings extends SettingSubpageProvider { autoLibraryTab: false, ); }, - active: settings.selectedLibraryTab.value == e.value, + active: settings.selectedLibraryTab.valueR == e.value, ), ), ), @@ -290,7 +290,7 @@ class ExtrasSettings extends SettingSubpageProvider { icon: Broken.document_filter, title: lang.ENABLE_SEARCH_CLEANUP, subtitle: lang.ENABLE_SEARCH_CLEANUP_SUBTITLE, - value: settings.enableSearchCleanup.value, + value: settings.enableSearchCleanup.valueR, onChanged: (p0) => settings.save(enableSearchCleanup: !p0), ), ), @@ -302,7 +302,7 @@ class ExtrasSettings extends SettingSubpageProvider { bgColor: getBgColor(_ExtraSettingsKeys.prioritizeEmbeddedLyrics), icon: Broken.mobile_programming, title: lang.PRIORITIZE_EMBEDDED_LYRICS, - value: settings.prioritizeEmbeddedLyrics.value, + value: settings.prioritizeEmbeddedLyrics.valueR, onChanged: (p0) => settings.save(prioritizeEmbeddedLyrics: !p0), ), ), @@ -312,13 +312,13 @@ class ExtrasSettings extends SettingSubpageProvider { child: Obx( () => CustomListTile( bgColor: getBgColor(_ExtraSettingsKeys.lyricsSource), - enabled: settings.enableLyrics.value, + enabled: settings.enableLyrics.valueR, title: lang.LYRICS_SOURCE, leading: const StackedIcon( baseIcon: Broken.mobile_programming, secondaryIcon: Broken.cpu_setting, ), - trailingText: settings.lyricsSource.value.toText(), + trailingText: settings.lyricsSource.valueR.toText(), onTap: () { bool isEnabled(LyricsSource val) => settings.lyricsSource.value == val; void tileOnTap(LyricsSource val) => settings.save(lyricsSource: val); @@ -367,7 +367,7 @@ class ExtrasSettings extends SettingSubpageProvider { icon: Broken.external_drive, title: lang.IMMERSIVE_MODE, subtitle: lang.IMMERSIVE_MODE_SUBTITLE, - value: settings.hideStatusBarInExpandedMiniplayer.value, + value: settings.hideStatusBarInExpandedMiniplayer.valueR, onChanged: (p0) => settings.save(hideStatusBarInExpandedMiniplayer: !p0), ), ), @@ -379,7 +379,7 @@ class ExtrasSettings extends SettingSubpageProvider { bgColor: getBgColor(_ExtraSettingsKeys.swipeToOpenDrawer), icon: Broken.sidebar_right, title: lang.SWIPE_TO_OPEN_DRAWER, - value: settings.swipeableDrawer.value, + value: settings.swipeableDrawer.valueR, onChanged: (isTrue) { settings.save(swipeableDrawer: !isTrue); NamidaNavigator.inst.innerDrawerKey.currentState?.toggleCanSwipe(!isTrue); @@ -395,7 +395,7 @@ class ExtrasSettings extends SettingSubpageProvider { icon: Broken.clipboard_export, title: lang.ENABLE_CLIPBOARD_MONITORING, subtitle: lang.ENABLE_CLIPBOARD_MONITORING_SUBTITLE, - value: settings.enableClipboardMonitoring.value, + value: settings.enableClipboardMonitoring.valueR, onChanged: (isTrue) { settings.save(enableClipboardMonitoring: !isTrue); }, @@ -411,8 +411,8 @@ class ExtrasSettings extends SettingSubpageProvider { trailing: Obx( () => Column( children: [ - Text("${Indexer.inst.colorPalettesInStorage.value}/${Indexer.inst.artworksInStorage.value}"), - if (CurrentColor.inst.isGeneratingAllColorPalettes.value) const LoadingIndicator(), + Text("${Indexer.inst.colorPalettesInStorage.valueR}/${Indexer.inst.artworksInStorage.valueR}"), + if (CurrentColor.inst.isGeneratingAllColorPalettes.valueR) const LoadingIndicator(), ], ), ), @@ -473,13 +473,14 @@ class ExtrasSettings extends SettingSubpageProvider { onTap: () { final subList = [].obs; - LibraryTab.values.loop((e, index) { + LibraryTab.values.loop((e) { if (!settings.libraryTabs.contains(e)) { subList.add(e); } }); NamidaNavigator.inst.navigateDialog( + scale: 1.0, onDisposing: () { subList.close(); }, @@ -492,8 +493,8 @@ class ExtrasSettings extends SettingSubpageProvider { ), ], child: SizedBox( - width: Get.width, - height: Get.height * 0.5, + width: namida.width, + height: namida.height * 0.5, child: Obx( () => Column( children: [ @@ -504,16 +505,15 @@ class ExtrasSettings extends SettingSubpageProvider { const SizedBox(height: 12.0), Expanded( flex: 6, - child: ReorderableListView.builder( - shrinkWrap: true, - proxyDecorator: (child, index, animation) => child, + child: NamidaListView( + itemExtent: null, padding: EdgeInsets.zero, itemCount: settings.libraryTabs.length, itemBuilder: (context, i) { final tab = settings.libraryTabs[i]; - return Container( + return Padding( key: ValueKey(i), - margin: const EdgeInsets.all(4.0), + padding: const EdgeInsets.all(4.0), child: ListTileWithCheckMark( title: "${i + 1}. ${tab.toText()}", icon: tab.toIcon(), @@ -534,7 +534,7 @@ class ExtrasSettings extends SettingSubpageProvider { if (newIndex > oldIndex) { newIndex -= 1; } - final item = settings.libraryTabs.elementAt(oldIndex); + final item = settings.libraryTabs.value.elementAt(oldIndex); settings.removeFromList( libraryTab1: item, ); diff --git a/lib/ui/widgets/settings/indexer_settings.dart b/lib/ui/widgets/settings/indexer_settings.dart index ed618b7d..eb1c0b68 100644 --- a/lib/ui/widgets/settings/indexer_settings.dart +++ b/lib/ui/widgets/settings/indexer_settings.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/base/setting_subpage_provider.dart'; import 'package:namida/controller/file_browser.dart'; @@ -10,12 +9,12 @@ import 'package:namida/controller/navigator_controller.dart'; import 'package:namida/controller/settings_controller.dart'; import 'package:namida/controller/tagger_controller.dart'; import 'package:namida/controller/video_controller.dart'; -import 'package:namida/core/constants.dart'; import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/pages/subpages/indexer_missing_tracks_subpage.dart'; import 'package:namida/ui/widgets/circular_percentages.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; @@ -94,7 +93,7 @@ class IndexerSettings extends SettingSubpageProvider { icon: Broken.airdrop, title: lang.USE_MEDIA_STORE, subtitle: lang.USE_MEDIA_STORE_SUBTITLE, - value: settings.useMediaStore.value, + value: settings.useMediaStore.valueR, onChanged: (isTrue) { settings.save(useMediaStore: !isTrue); showRefreshPromptDialog(false); @@ -113,7 +112,7 @@ class IndexerSettings extends SettingSubpageProvider { icon: Broken.backward_item, title: lang.GROUP_ARTWORKS_BY_ALBUM, subtitle: lang.REQUIRES_CLEARING_IMAGE_CACHE_AND_RE_INDEXING, - value: settings.groupArtworksByAlbum.value, + value: settings.groupArtworksByAlbum.valueR, onChanged: (isTrue) { settings.save(groupArtworksByAlbum: !isTrue); _showReindexingPrompt(title: lang.GROUP_ARTWORKS_BY_ALBUM, body: lang.REQUIRES_CLEARING_IMAGE_CACHE_AND_RE_INDEXING); @@ -132,7 +131,7 @@ class IndexerSettings extends SettingSubpageProvider { child: Obx( () => AnimatedOpacity( duration: const Duration(milliseconds: 200), - opacity: settings.useMediaStore.value ? 0.5 : 1.0, + opacity: settings.useMediaStore.valueR ? 0.5 : 1.0, child: NamidaExpansionTile( bgColor: getBgColor(_IndexerSettingsKeys.foldersToScan), initiallyExpanded: initiallyExpanded, @@ -143,7 +142,7 @@ class IndexerSettings extends SettingSubpageProvider { mainAxisSize: MainAxisSize.min, children: [ IgnorePointer( - ignoring: settings.useMediaStore.value, + ignoring: settings.useMediaStore.valueR, child: addFolderButton((dirsPath) { settings.save(directoriesToScan: dirsPath); }), @@ -153,7 +152,7 @@ class IndexerSettings extends SettingSubpageProvider { ], ), children: [ - ...settings.directoriesToScan.map( + ...settings.directoriesToScan.valueR.map( (e) => IgnorePointer( ignoring: settings.useMediaStore.value, child: ListTile( @@ -190,7 +189,10 @@ class IndexerSettings extends SettingSubpageProvider { ); } }, - child: Text(lang.REMOVE.toUpperCase()), + child: NamidaButtonText( + lang.REMOVE.toUpperCase(), + style: const TextStyle(fontSize: 14.0), + ), ), ), ), @@ -208,8 +210,9 @@ class IndexerSettings extends SettingSubpageProvider { }) { return getItemWrapper( key: _IndexerSettingsKeys.foldersToExclude, - child: Obx( - () => NamidaExpansionTile( + child: ObxO( + rx: settings.directoriesToExclude, + builder: (directoriesToExclude) => NamidaExpansionTile( bgColor: getBgColor(_IndexerSettingsKeys.foldersToExclude), initiallyExpanded: initiallyExpanded, icon: Broken.folder_minus, @@ -225,7 +228,7 @@ class IndexerSettings extends SettingSubpageProvider { const Icon(Broken.arrow_down_2), ], ), - children: settings.directoriesToExclude.isEmpty + children: directoriesToExclude.isEmpty ? [ ListTile( title: Text( @@ -235,7 +238,7 @@ class IndexerSettings extends SettingSubpageProvider { ), ] : [ - ...settings.directoriesToExclude.map( + ...directoriesToExclude.map( (e) => ListTile( title: Text( e, @@ -246,7 +249,10 @@ class IndexerSettings extends SettingSubpageProvider { settings.removeFromList(directoriesToExclude1: e); showRefreshPromptDialog(true); }, - child: Text(lang.REMOVE.toUpperCase()), + child: NamidaButtonText( + lang.REMOVE.toUpperCase(), + style: const TextStyle(fontSize: 14.0), + ), ), ), ), @@ -309,26 +315,31 @@ class IndexerSettings extends SettingSubpageProvider { SizedBox( height: 50, child: FittedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Obx( - () => StatsContainer( - icon: Broken.info_circle, - title: '${lang.TRACKS_INFO} :', - value: allTracksInLibrary.length.formatDecimal(), - total: Indexer.inst.allAudioFiles.isEmpty ? null : Indexer.inst.allAudioFiles.length.formatDecimal(), + child: ObxO( + rx: Indexer.inst.allAudioFiles, + builder: (allAudioFiles) => Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ObxO( + rx: Indexer.inst.tracksInfoList, + builder: (tracksInfoList) => StatsContainer( + icon: Broken.info_circle, + title: '${lang.TRACKS_INFO} :', + value: tracksInfoList.length.formatDecimal(), + total: allAudioFiles.isEmpty ? null : allAudioFiles.length.formatDecimal(), + ), ), - ), - Obx( - () => StatsContainer( - icon: Broken.image, - title: '${lang.ARTWORKS} :', - value: Indexer.inst.artworksInStorage.value.formatDecimal(), - total: Indexer.inst.allAudioFiles.isEmpty ? null : Indexer.inst.allAudioFiles.length.formatDecimal(), + ObxO( + rx: Indexer.inst.artworksInStorage, + builder: (artworksInStorage) => StatsContainer( + icon: Broken.image, + title: '${lang.ARTWORKS} :', + value: artworksInStorage.formatDecimal(), + total: allAudioFiles.isEmpty ? null : allAudioFiles.length.formatDecimal(), + ), ), - ), - ], + ], + ), ), ), ), @@ -343,7 +354,7 @@ class IndexerSettings extends SettingSubpageProvider { padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0), child: Obx( () => Text( - '${lang.DUPLICATED_TRACKS}: ${Indexer.inst.duplicatedTracksLength.value}\n${lang.TRACKS_EXCLUDED_BY_NOMEDIA}: ${Indexer.inst.tracksExcludedByNoMedia.value}\n${lang.FILTERED_BY_SIZE_AND_DURATION}: ${Indexer.inst.filteredForSizeDurationTracks.value}', + '${lang.DUPLICATED_TRACKS}: ${Indexer.inst.duplicatedTracksLength.valueR}\n${lang.TRACKS_EXCLUDED_BY_NOMEDIA}: ${Indexer.inst.tracksExcludedByNoMedia.valueR}\n${lang.FILTERED_BY_SIZE_AND_DURATION}: ${Indexer.inst.filteredForSizeDurationTracks.valueR}', style: context.textTheme.displaySmall, ), ), @@ -358,7 +369,7 @@ class IndexerSettings extends SettingSubpageProvider { title: lang.PREVENT_DUPLICATED_TRACKS, subtitle: "${lang.PREVENT_DUPLICATED_TRACKS_SUBTITLE}. ${lang.INDEX_REFRESH_REQUIRED}", onChanged: (isTrue) => settings.save(preventDuplicatedTracks: !isTrue), - value: settings.preventDuplicatedTracks.value, + value: settings.preventDuplicatedTracks.valueR, ), ), ), @@ -367,12 +378,12 @@ class IndexerSettings extends SettingSubpageProvider { child: Obx( () => CustomSwitchListTile( bgColor: getBgColor(_IndexerSettingsKeys.respectNoMedia), - enabled: !settings.useMediaStore.value, + enabled: !settings.useMediaStore.valueR, icon: Broken.cd, title: lang.RESPECT_NO_MEDIA, subtitle: "${lang.RESPECT_NO_MEDIA_SUBTITLE}. ${lang.INDEX_REFRESH_REQUIRED}", onChanged: (isTrue) => settings.save(respectNoMedia: !isTrue), - value: settings.useMediaStore.value ? false : settings.respectNoMedia.value, + value: settings.useMediaStore.valueR ? false : settings.respectNoMedia.valueR, ), ), ), @@ -388,7 +399,7 @@ class IndexerSettings extends SettingSubpageProvider { settings.save(extractFeatArtistFromTitle: !isTrue); await Indexer.inst.prepareTracksFile(); }, - value: settings.extractFeatArtistFromTitle.value, + value: settings.extractFeatArtistFromTitle.valueR, ), ), ), @@ -402,7 +413,7 @@ class IndexerSettings extends SettingSubpageProvider { title: lang.ALBUM_IDENTIFIERS, trailingText: settings.albumIdentifiers.length.toString(), onTap: () { - final tempList = List.from(settings.albumIdentifiers).obs; + final tempList = List.from(settings.albumIdentifiers.value).obs; NamidaNavigator.inst.navigateDialog( onDisposing: () { tempList.close(); @@ -415,13 +426,13 @@ class IndexerSettings extends SettingSubpageProvider { Obx( () { return NamidaButton( - enabled: settings.albumIdentifiers.any((element) => !tempList.contains(element)) || - tempList.any((element) => !settings.albumIdentifiers.contains(element)), // isEqualTo wont work cuz order shouldnt matter + enabled: settings.albumIdentifiers.valueR.any((element) => !tempList.contains(element)) || + tempList.valueR.any((element) => !settings.albumIdentifiers.contains(element)), // isEqualTo wont work cuz order shouldnt matter text: lang.SAVE, onPressed: () async { NamidaNavigator.inst.closeDialog(); settings.removeFromList(albumIdentifiersAll: AlbumIdentifier.values); - settings.save(albumIdentifiers: tempList); + settings.save(albumIdentifiers: tempList.value); Indexer.inst.prepareTracksFile(); @@ -511,12 +522,12 @@ class IndexerSettings extends SettingSubpageProvider { trailing: NamidaWheelSlider( width: 100.0, totalCount: 1024, - initValue: settings.indexMinFileSizeInB.value.toInt() / 1024 ~/ 10, + initValue: settings.indexMinFileSizeInB.valueR.toInt() / 1024 ~/ 10, onValueChanged: (val) { final d = val; settings.save(indexMinFileSizeInB: d * 1024 * 10); }, - text: settings.indexMinFileSizeInB.value.fileSizeFormatted, + text: settings.indexMinFileSizeInB.valueR.fileSizeFormatted, ), ), ), @@ -532,12 +543,12 @@ class IndexerSettings extends SettingSubpageProvider { trailing: NamidaWheelSlider( width: 100.0, totalCount: 180, - initValue: settings.indexMinDurationInSec.value, + initValue: settings.indexMinDurationInSec.valueR, onValueChanged: (val) { final d = val; settings.save(indexMinDurationInSec: d); }, - text: "${settings.indexMinDurationInSec.value} s", + text: "${settings.indexMinDurationInSec.valueR} s", ), ), ), @@ -550,7 +561,7 @@ class IndexerSettings extends SettingSubpageProvider { bgColor: getBgColor(_IndexerSettingsKeys.refreshOnStartup), icon: Broken.d_rotate, title: lang.REFRESH_ON_STARTUP, - value: settings.refreshOnStartup.value, + value: settings.refreshOnStartup.valueR, onChanged: (isTrue) => settings.save(refreshOnStartup: !isTrue), ), ), @@ -605,14 +616,18 @@ class IndexerSettings extends SettingSubpageProvider { style: context.textTheme.displayMedium, ), const SizedBox(height: 16.0), - Obx( - () => ListTileWithCheckMark( - dense: true, - icon: Broken.trash, - title: lang.CLEAR_IMAGE_CACHE, - subtitle: Indexer.inst.artworksSizeInStorage.value.fileSizeFormatted, - active: clearArtworks.value, - onTap: () => clearArtworks.value = !clearArtworks.value, + ObxO( + rx: Indexer.inst.artworksSizeInStorage, + builder: (artworksSizeInStorage) => ObxO( + rx: clearArtworks, + builder: (active) => ListTileWithCheckMark( + dense: true, + icon: Broken.trash, + title: lang.CLEAR_IMAGE_CACHE, + subtitle: artworksSizeInStorage.fileSizeFormatted, + active: active, + onTap: clearArtworks.toggle, + ), ), ), ], @@ -653,7 +668,7 @@ class IndexerSettings extends SettingSubpageProvider { final TextEditingController separatorsController = TextEditingController(); final isBlackListDialog = trackArtistsSeparatorsBlacklist || trackGenresSeparatorsBlacklist; - final RxBool updatingLibrary = false.obs; + final updatingLibrary = false.obs; NamidaNavigator.inst.navigateDialog( onDisposing: () { @@ -696,7 +711,7 @@ class IndexerSettings extends SettingSubpageProvider { ), if (isBlackListDialog) const CancelButton(), Obx( - () => updatingLibrary.value + () => updatingLibrary.valueR ? const LoadingIndicator() : NamidaButton( text: lang.ADD, @@ -727,7 +742,7 @@ class IndexerSettings extends SettingSubpageProvider { children: [ Text( isBlackListDialog ? lang.SEPARATORS_BLACKLIST_SUBTITLE : lang.SEPARATORS_MESSAGE, - style: Get.textTheme.displaySmall, + style: namida.textTheme.displaySmall, ), const SizedBox( height: 12.0, @@ -735,12 +750,12 @@ class IndexerSettings extends SettingSubpageProvider { Obx( () => Wrap( children: [ - ...itemsList.map( + ...itemsList.valueR.map( (e) => Container( margin: const EdgeInsets.all(4.0), padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 10.0), decoration: BoxDecoration( - color: Get.theme.cardTheme.color, + color: namida.theme.cardTheme.color, borderRadius: BorderRadius.circular(16.0.multipliedRadius), ), child: InkWell( @@ -783,17 +798,17 @@ class IndexerSettings extends SettingSubpageProvider { Padding( padding: const EdgeInsets.only(top: 14.0), child: TextField( - style: Get.textTheme.displaySmall?.copyWith(fontSize: 16.0.multipliedFontScale, fontWeight: FontWeight.w500), + style: namida.textTheme.displaySmall?.copyWith(fontSize: 16.0, fontWeight: FontWeight.w500), decoration: InputDecoration( errorMaxLines: 3, isDense: true, focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(14.0.multipliedRadius), - borderSide: BorderSide(color: Get.theme.colorScheme.onBackground.withAlpha(100), width: 2.0), + borderSide: BorderSide(color: namida.theme.colorScheme.onSurface.withAlpha(100), width: 2.0), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(18.0.multipliedRadius), - borderSide: BorderSide(color: Get.theme.colorScheme.onBackground.withAlpha(100), width: 1.0), + borderSide: BorderSide(color: namida.theme.colorScheme.onSurface.withAlpha(100), width: 1.0), ), hintText: lang.VALUE, ), @@ -959,7 +974,7 @@ class __ExtractingPathsWidgetState extends State<_ExtractingPathsWidget> { e, maxLines: _isPathsExpanded ? null : 1, overflow: _isPathsExpanded ? null : TextOverflow.ellipsis, - style: context.textTheme.displaySmall?.copyWith(fontSize: 11.0.multipliedFontScale), + style: context.textTheme.displaySmall?.copyWith(fontSize: 11.0), ), )) .toList(), diff --git a/lib/ui/widgets/settings/playback_settings.dart b/lib/ui/widgets/settings/playback_settings.dart index 14bc38b2..c6313938 100644 --- a/lib/ui/widgets/settings/playback_settings.dart +++ b/lib/ui/widgets/settings/playback_settings.dart @@ -1,8 +1,6 @@ import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - import 'package:namida/base/setting_subpage_provider.dart'; import 'package:namida/controller/navigator_controller.dart'; import 'package:namida/controller/player_controller.dart'; @@ -15,6 +13,7 @@ import 'package:namida/core/functions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/settings_card.dart'; @@ -86,7 +85,7 @@ class PlaybackSettings extends SettingSubpageProvider { bgColor: getBgColor(_PlaybackSettingsKeys.enableVideoPlayback), title: lang.ENABLE_VIDEO_PLAYBACK, icon: Broken.video, - value: settings.enableVideoPlayback.value, + value: settings.enableVideoPlayback.valueR, onChanged: (p0) async => await VideoController.inst.toggleVideoPlayback(), ), ), @@ -96,10 +95,10 @@ class PlaybackSettings extends SettingSubpageProvider { child: Obx( () => CustomListTile( bgColor: getBgColor(_PlaybackSettingsKeys.videoSource), - enabled: settings.enableVideoPlayback.value, + enabled: settings.enableVideoPlayback.valueR, title: lang.VIDEO_PLAYBACK_SOURCE, icon: Broken.scroll, - trailingText: settings.videoPlaybackSource.value.toText(), + trailingText: settings.videoPlaybackSource.valueR.toText(), onTap: () { bool isEnabled(VideoPlaybackSource val) => settings.videoPlaybackSource.value == val; void tileOnTap(VideoPlaybackSource val) => settings.save(videoPlaybackSource: val); @@ -146,10 +145,10 @@ class PlaybackSettings extends SettingSubpageProvider { child: Obx( () => CustomListTile( bgColor: getBgColor(_PlaybackSettingsKeys.videoQuality), - enabled: settings.enableVideoPlayback.value, + enabled: settings.enableVideoPlayback.valueR, title: lang.VIDEO_QUALITY, icon: Broken.story, - trailingText: settings.youtubeVideoQualities.first, + trailingText: settings.youtubeVideoQualities.valueR.first, onTap: () { bool isEnabled(String val) => settings.youtubeVideoQualities.contains(val); @@ -165,7 +164,7 @@ class PlaybackSettings extends SettingSubpageProvider { } // sorts and saves dec settings.youtubeVideoQualities.sortByReverse((e) => kStockVideoQualities.indexOf(e)); - settings.save(youtubeVideoQualities: settings.youtubeVideoQualities); + settings.save(youtubeVideoQualities: settings.youtubeVideoQualities.value); } NamidaNavigator.inst.navigateDialog( @@ -193,8 +192,8 @@ class PlaybackSettings extends SettingSubpageProvider { Text("${lang.NOTE}: ${lang.VIDEO_QUALITY_SUBTITLE_NOTE}"), const SizedBox(height: 18.0), SizedBox( - width: Get.width, - height: Get.height * 0.4, + width: namida.width, + height: namida.height * 0.4, child: ListView( padding: EdgeInsets.zero, children: [ @@ -227,10 +226,10 @@ class PlaybackSettings extends SettingSubpageProvider { child: Obx( () => CustomListTile( bgColor: getBgColor(_PlaybackSettingsKeys.localVideoMatching), - enabled: settings.enableVideoPlayback.value, + enabled: settings.enableVideoPlayback.valueR, icon: Broken.video_tick, title: lang.LOCAL_VIDEO_MATCHING, - trailingText: settings.localVideoMatchingType.value.toText(), + trailingText: settings.localVideoMatchingType.valueR.toText(), onTap: () { NamidaNavigator.inst.navigateDialog( dialog: CustomBlurryDialog( @@ -247,7 +246,7 @@ class PlaybackSettings extends SettingSubpageProvider { () => CustomListTile( icon: Broken.video_tick, title: lang.MATCHING_TYPE, - trailingText: settings.localVideoMatchingType.value.toText(), + trailingText: settings.localVideoMatchingType.valueR.toText(), onTap: () { final e = settings.localVideoMatchingType.value.nextElement(LocalVideoMatchingType.values); settings.save(localVideoMatchingType: e); @@ -258,7 +257,7 @@ class PlaybackSettings extends SettingSubpageProvider { () => CustomSwitchListTile( icon: Broken.folder, title: lang.SAME_DIRECTORY_ONLY, - value: settings.localVideoMatchingCheckSameDir.value, + value: settings.localVideoMatchingCheckSameDir.valueR, onChanged: (isTrue) => settings.save(localVideoMatchingCheckSameDir: !isTrue), ), ), @@ -276,7 +275,7 @@ class PlaybackSettings extends SettingSubpageProvider { () => CustomListTile( bgColor: getBgColor(_PlaybackSettingsKeys.keepScreenAwake), title: '${lang.KEEP_SCREEN_AWAKE_WHEN}:', - subtitle: settings.wakelockMode.value.toText(), + subtitle: settings.wakelockMode.valueR.toText(), icon: Broken.external_drive, onTap: () { final e = settings.wakelockMode.value.nextElement(WakelockMode.values); @@ -292,7 +291,7 @@ class PlaybackSettings extends SettingSubpageProvider { bgColor: getBgColor(_PlaybackSettingsKeys.displayFavButtonInNotif), title: lang.DISPLAY_FAV_BUTTON_IN_NOTIFICATION, icon: Broken.heart_tick, - value: settings.displayFavouriteButtonInNotification.value, + value: settings.displayFavouriteButtonInNotification.valueR, onChanged: (val) { settings.save(displayFavouriteButtonInNotification: !val); Player.inst.refreshNotification(); @@ -314,7 +313,7 @@ class PlaybackSettings extends SettingSubpageProvider { baseIcon: Broken.gallery, secondaryIcon: Broken.lock_circle, ), - value: settings.player.lockscreenArtwork.value, + value: settings.player.lockscreenArtwork.valueR, onChanged: (val) { settings.player.save(lockscreenArtwork: !val); AudioService.setLockScreenArtwork(!val).then((_) => Player.inst.refreshNotification()); @@ -333,7 +332,7 @@ class PlaybackSettings extends SettingSubpageProvider { final element = settings.player.killAfterDismissingApp.value.nextElement(KillAppMode.values); settings.player.save(killAfterDismissingApp: element); }, - trailingText: settings.player.killAfterDismissingApp.value.toText(), + trailingText: settings.player.killAfterDismissingApp.valueR.toText(), ), ), ), @@ -343,7 +342,7 @@ class PlaybackSettings extends SettingSubpageProvider { () => CustomListTile( bgColor: getBgColor(_PlaybackSettingsKeys.onNotificationTap), title: lang.ON_NOTIFICATION_TAP, - trailingText: settings.onNotificationTapAction.value.toText(), + trailingText: settings.onNotificationTapAction.valueR.toText(), icon: Broken.card, onTap: () { final element = settings.onNotificationTapAction.value.nextElement(NotificationTapAction.values); @@ -360,7 +359,7 @@ class PlaybackSettings extends SettingSubpageProvider { icon: Broken.sidebar_bottom, title: lang.DISMISSIBLE_MINIPLAYER, onChanged: (value) => settings.save(dismissibleMiniplayer: !value), - value: settings.dismissibleMiniplayer.value, + value: settings.dismissibleMiniplayer.valueR, ), ), ), @@ -376,7 +375,7 @@ class PlaybackSettings extends SettingSubpageProvider { settings.player.save(skipSilenceEnabled: willBeTrue); await Player.inst.setSkipSilenceEnabled(willBeTrue); }, - value: settings.player.skipSilenceEnabled.value, + value: settings.player.skipSilenceEnabled.valueR, ), ), ), @@ -398,44 +397,46 @@ class PlaybackSettings extends SettingSubpageProvider { if (!wasCollapsed) return settings.player.save(enableCrossFade: false); SussyBaka.monetize(onEnable: () => settings.player.save(enableCrossFade: true)); }, - trailing: Obx(() => CustomSwitch(active: settings.player.enableCrossFade.value)), + trailing: Obx(() => CustomSwitch(active: settings.player.enableCrossFade.valueR)), children: [ Obx( () { const stepper = 100; const minVal = 100; + final enableCrossFade = settings.player.enableCrossFade.valueR; + final crossFadeDurationMS = settings.player.crossFadeDurationMS.valueR; return CustomListTile( - enabled: settings.player.enableCrossFade.value, + enabled: enableCrossFade, icon: Broken.blend_2, title: lang.CROSSFADE_DURATION, trailing: NamidaWheelSlider( totalCount: (10000 - minVal) ~/ stepper, - initValue: settings.player.crossFadeDurationMS.value ~/ stepper, + initValue: crossFadeDurationMS ~/ stepper, onValueChanged: (val) { final v = (val * stepper + minVal); settings.player.save(crossFadeDurationMS: v); }, - text: settings.player.crossFadeDurationMS.value >= 1000 - ? "${settings.player.crossFadeDurationMS.value / 1000}s" - : "${settings.player.crossFadeDurationMS.value}ms", + text: crossFadeDurationMS >= 1000 ? "${crossFadeDurationMS / 1000}s" : "${crossFadeDurationMS}ms", ), ); }, ), Obx( () { - final val = settings.player.crossFadeAutoTriggerSeconds.value; + final crossFadeAutoTriggerSeconds = settings.player.crossFadeAutoTriggerSeconds.valueR; return CustomListTile( - enabled: settings.player.enableCrossFade.value, + enabled: settings.player.enableCrossFade.valueR, icon: Broken.blend, - title: val == 0 ? lang.CROSSFADE_TRIGGER_SECONDS_DISABLED : lang.CROSSFADE_TRIGGER_SECONDS.replaceFirst('_SECONDS_', "$val"), + title: crossFadeAutoTriggerSeconds == 0 + ? lang.CROSSFADE_TRIGGER_SECONDS_DISABLED + : lang.CROSSFADE_TRIGGER_SECONDS.replaceFirst('_SECONDS_', "$crossFadeAutoTriggerSeconds"), trailing: NamidaWheelSlider( totalCount: 30, - initValue: val, + initValue: crossFadeAutoTriggerSeconds, onValueChanged: (val) { settings.player.save(crossFadeAutoTriggerSeconds: val); }, - text: "${val}s", + text: "${crossFadeAutoTriggerSeconds}s", ), ); }, @@ -461,37 +462,37 @@ class PlaybackSettings extends SettingSubpageProvider { settings.player.save(enableVolumeFadeOnPlayPause: value); Player.inst.setVolume(settings.player.volume.value); }, - trailing: Obx(() => CustomSwitch(active: settings.player.enableVolumeFadeOnPlayPause.value)), + trailing: Obx(() => CustomSwitch(active: settings.player.enableVolumeFadeOnPlayPause.valueR)), children: [ Obx( () => CustomListTile( - enabled: settings.player.enableVolumeFadeOnPlayPause.value, + enabled: settings.player.enableVolumeFadeOnPlayPause.valueR, icon: Broken.play, title: lang.PLAY_FADE_DURATION, trailing: NamidaWheelSlider( totalCount: 1900 ~/ 50, - initValue: settings.player.playFadeDurInMilli.value ~/ 50, + initValue: settings.player.playFadeDurInMilli.valueR ~/ 50, onValueChanged: (val) { final v = (val * 50 + 100); settings.player.save(playFadeDurInMilli: v); }, - text: "${settings.player.playFadeDurInMilli.value}ms", + text: "${settings.player.playFadeDurInMilli.valueR}ms", ), ), ), Obx( () => CustomListTile( - enabled: settings.player.enableVolumeFadeOnPlayPause.value, + enabled: settings.player.enableVolumeFadeOnPlayPause.valueR, icon: Broken.pause, title: lang.PAUSE_FADE_DURATION, trailing: NamidaWheelSlider( totalCount: 1900 ~/ 50, - initValue: settings.player.pauseFadeDurInMilli.value ~/ 50, + initValue: settings.player.pauseFadeDurInMilli.valueR ~/ 50, onValueChanged: (val) { final v = (val * 50 + 100); settings.player.save(pauseFadeDurInMilli: v); }, - text: "${settings.player.pauseFadeDurInMilli.value}ms", + text: "${settings.player.pauseFadeDurInMilli.valueR}ms", ), ), ), @@ -509,7 +510,7 @@ class PlaybackSettings extends SettingSubpageProvider { ), title: lang.PLAY_AFTER_NEXT_PREV, onChanged: (value) => settings.player.save(playOnNextPrev: !value), - value: settings.player.playOnNextPrev.value, + value: settings.player.playOnNextPrev.valueR, ), ), ), @@ -522,7 +523,7 @@ class PlaybackSettings extends SettingSubpageProvider { title: lang.INFINITY_QUEUE_ON_NEXT_PREV, subtitle: lang.INFINITY_QUEUE_ON_NEXT_PREV_SUBTITLE, onChanged: (value) => settings.player.save(infiniyQueueOnNextPrevious: !value), - value: settings.player.infiniyQueueOnNextPrevious.value, + value: settings.player.infiniyQueueOnNextPrevious.valueR, ), ), ), @@ -540,13 +541,13 @@ class PlaybackSettings extends SettingSubpageProvider { icon: Broken.pause_circle, title: lang.PAUSE_PLAYBACK, onChanged: (value) => settings.player.save(pauseOnVolume0: !value), - value: settings.player.pauseOnVolume0.value, + value: settings.player.pauseOnVolume0.valueR, ), ), Obx( () { - final valInSet = settings.player.volume0ResumeThresholdMin.value; - final disabled = !settings.player.resumeAfterOnVolume0Pause.value; + final valInSet = settings.player.volume0ResumeThresholdMin.valueR; + final disabled = !settings.player.resumeAfterOnVolume0Pause.valueR; const max = 61; return CustomListTile( icon: Broken.play_circle, @@ -554,7 +555,7 @@ class PlaybackSettings extends SettingSubpageProvider { ? lang.DONT_RESUME : valInSet == 0 ? lang.RESUME_IF_WAS_PAUSED_BY_VOLUME - : lang.RESUME_IF_WAS_PAUSED_FOR_LESS_THAN_N_MIN.replaceFirst('_NUM_', "${settings.player.volume0ResumeThresholdMin.value}"), + : lang.RESUME_IF_WAS_PAUSED_FOR_LESS_THAN_N_MIN.replaceFirst('_NUM_', "${settings.player.volume0ResumeThresholdMin.valueR}"), trailing: NamidaWheelSlider( totalCount: max, initValue: valInSet, @@ -632,8 +633,8 @@ class PlaybackSettings extends SettingSubpageProvider { const SizedBox(height: 6.0), Obx( () { - final valInSet = settings.player.interruptionResumeThresholdMin.value; - final disabled = !settings.player.resumeAfterWasInterrupted.value; + final valInSet = settings.player.interruptionResumeThresholdMin.valueR; + final disabled = !settings.player.resumeAfterWasInterrupted.valueR; const max = 61; return CustomListTile( icon: Broken.play_circle, @@ -641,7 +642,7 @@ class PlaybackSettings extends SettingSubpageProvider { ? lang.DONT_RESUME : valInSet == 0 ? lang.RESUME_IF_WAS_INTERRUPTED - : lang.RESUME_IF_WAS_PAUSED_FOR_LESS_THAN_N_MIN.replaceFirst('_NUM_', "${settings.player.interruptionResumeThresholdMin.value}"), + : lang.RESUME_IF_WAS_PAUSED_FOR_LESS_THAN_N_MIN.replaceFirst('_NUM_', "${settings.player.interruptionResumeThresholdMin.valueR}"), trailing: NamidaWheelSlider( totalCount: max, initValue: valInSet, @@ -673,7 +674,7 @@ class PlaybackSettings extends SettingSubpageProvider { icon: Broken.rotate_left, title: lang.JUMP_TO_FIRST_TRACK_AFTER_QUEUE_FINISH, onChanged: (value) => settings.player.save(jumpToFirstTrackAfterFinishingQueue: !value), - value: settings.player.jumpToFirstTrackAfterFinishingQueue.value, + value: settings.player.jumpToFirstTrackAfterFinishingQueue.valueR, ), ), ), @@ -690,7 +691,7 @@ class PlaybackSettings extends SettingSubpageProvider { title: lang.PREVIOUS_BUTTON_REPLAYS, subtitle: lang.PREVIOUS_BUTTON_REPLAYS_SUBTITLE, onChanged: (value) => settings.save(previousButtonReplays: !value), - value: settings.previousButtonReplays.value, + value: settings.previousButtonReplays.valueR, ), ), ), @@ -700,27 +701,25 @@ class PlaybackSettings extends SettingSubpageProvider { () => CustomListTile( bgColor: getBgColor(_PlaybackSettingsKeys.seekDuration), icon: Broken.forward_5_seconds, - title: "${lang.SEEK_DURATION} (${settings.player.isSeekDurationPercentage.value ? lang.PERCENTAGE : lang.SECONDS})", + title: "${lang.SEEK_DURATION} (${settings.player.isSeekDurationPercentage.valueR ? lang.PERCENTAGE : lang.SECONDS})", subtitle: lang.SEEK_DURATION_INFO, onTap: () => settings.player.save(isSeekDurationPercentage: !settings.player.isSeekDurationPercentage.value), - trailing: settings.player.isSeekDurationPercentage.value + trailing: settings.player.isSeekDurationPercentage.valueR ? NamidaWheelSlider( totalCount: 50, - initValue: settings.player.seekDurationInPercentage.value, + initValue: settings.player.seekDurationInPercentage.valueR, onValueChanged: (val) { - final v = (val) as int; - settings.player.save(seekDurationInPercentage: v); + settings.player.save(seekDurationInPercentage: val); }, - text: "${settings.player.seekDurationInPercentage.value}%", + text: "${settings.player.seekDurationInPercentage.valueR}%", ) : NamidaWheelSlider( totalCount: 120, - initValue: settings.player.seekDurationInSeconds.value, + initValue: settings.player.seekDurationInSeconds.valueR, onValueChanged: (val) { - final v = (val) as int; - settings.player.save(seekDurationInSeconds: v); + settings.player.save(seekDurationInSeconds: val); }, - text: "${settings.player.seekDurationInSeconds.value}s", + text: "${settings.player.seekDurationInSeconds.valueR}s", ), ), ), @@ -729,7 +728,7 @@ class PlaybackSettings extends SettingSubpageProvider { key: _PlaybackSettingsKeys.minimumTrackDurToRestoreLastPosition, child: Obx( () { - final valInSet = settings.player.minTrackDurationToRestoreLastPosInMinutes.value; + final valInSet = settings.player.minTrackDurationToRestoreLastPosInMinutes.valueR; const max = 121; return CustomListTile( bgColor: getBgColor(_PlaybackSettingsKeys.minimumTrackDurToRestoreLastPosition), @@ -776,13 +775,13 @@ class PlaybackSettings extends SettingSubpageProvider { children: [ NamidaWheelSlider( totalCount: 160, - initValue: settings.isTrackPlayedSecondsCount.value - 20, + initValue: settings.isTrackPlayedSecondsCount.valueR - 20, onValueChanged: (val) { final v = (val + 20); settings.save(isTrackPlayedSecondsCount: v); }, - text: "${settings.isTrackPlayedSecondsCount.value}s", - topText: lang.SECONDS.capitalizeFirst, + text: "${settings.isTrackPlayedSecondsCount.valueR}s", + topText: lang.SECONDS.capitalizeFirst(), textPadding: 8.0, ), Text( @@ -791,12 +790,12 @@ class PlaybackSettings extends SettingSubpageProvider { ), NamidaWheelSlider( totalCount: 80, - initValue: settings.isTrackPlayedPercentageCount.value - 20, + initValue: settings.isTrackPlayedPercentageCount.valueR - 20, onValueChanged: (val) { final v = (val + 20); settings.save(isTrackPlayedPercentageCount: v); }, - text: "${settings.isTrackPlayedPercentageCount.value}%", + text: "${settings.isTrackPlayedPercentageCount.valueR}%", topText: lang.PERCENTAGE, textPadding: 8.0, ), @@ -807,7 +806,7 @@ class PlaybackSettings extends SettingSubpageProvider { ), ), ), - trailingText: "${settings.isTrackPlayedSecondsCount.value}s | ${settings.isTrackPlayedPercentageCount.value}%", + trailingText: "${settings.isTrackPlayedSecondsCount.valueR}s | ${settings.isTrackPlayedPercentageCount.valueR}%", ), ), ), diff --git a/lib/ui/widgets/settings/theme_settings.dart b/lib/ui/widgets/settings/theme_settings.dart index 8acb4ae3..75c563af 100644 --- a/lib/ui/widgets/settings/theme_settings.dart +++ b/lib/ui/widgets/settings/theme_settings.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; -import 'package:get/get.dart'; import 'package:namida/base/setting_subpage_provider.dart'; import 'package:namida/class/lang.dart'; @@ -19,6 +18,7 @@ import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/themes.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/settings_card.dart'; @@ -52,10 +52,11 @@ class ThemeSetting extends SettingSubpageProvider { }; void _refreshColorCurrentPlayingItem() { - if (Player.inst.nowPlayingVideoID != null) { - CurrentColor.inst.updatePlayerColorFromYoutubeID(Player.inst.nowPlayingVideoID!); + final currentVideoId = Player.inst.currentVideo; + if (currentVideoId != null) { + CurrentColor.inst.updatePlayerColorFromYoutubeID(currentVideoId); } else { - CurrentColor.inst.updatePlayerColorFromTrack(Player.inst.nowPlayingTWD, null); + CurrentColor.inst.updatePlayerColorFromTrack(Player.inst.currentTrack, null); } } @@ -74,14 +75,15 @@ class ThemeSetting extends SettingSubpageProvider { Widget getLanguageTile(BuildContext context) { return getItemWrapper( key: _ThemeSettingsKeys.language, - child: Obx( - () => CustomListTile( + child: ObxO( + rx: lang.currentLanguage, + builder: (currentLanguage) => CustomListTile( bgColor: getBgColor(_ThemeSettingsKeys.language), icon: Broken.language_square, title: lang.LANGUAGE, - subtitle: lang.currentLanguage.name, + subtitle: currentLanguage.name, onTap: () { - final Rx selectedLang = lang.currentLanguage.obs; + final Rx selectedLang = lang.currentLanguage.value.obs; NamidaNavigator.inst.navigateDialog( onDisposing: () { selectedLang.close(); @@ -118,8 +120,8 @@ class ThemeSetting extends SettingSubpageProvider { ) ], child: SizedBox( - height: Get.height * 0.5, - width: Get.width, + height: namida.height * 0.5, + width: namida.width, child: SingleChildScrollView( child: Column( children: [ @@ -135,7 +137,7 @@ class ThemeSetting extends SettingSubpageProvider { shape: BoxShape.circle, border: Border.all( width: 1.5, - color: context.theme.colorScheme.onBackground.withAlpha(100), + color: context.theme.colorScheme.onSurface.withAlpha(100), ), ), child: Text( @@ -143,8 +145,8 @@ class ThemeSetting extends SettingSubpageProvider { style: const TextStyle(fontSize: 13.0), ), ), - titleWidget: RichText( - text: TextSpan( + titleWidget: Text.rich( + TextSpan( text: e.name, style: context.textTheme.displayMedium, children: [ @@ -155,7 +157,7 @@ class ThemeSetting extends SettingSubpageProvider { ], ), ), - active: e == selectedLang.value, + active: e == selectedLang.valueR, onTap: () => selectedLang.value = e, ), ), @@ -201,8 +203,8 @@ class ThemeSetting extends SettingSubpageProvider { icon: Broken.colorfilter, title: lang.AUTO_COLORING, subtitle: lang.AUTO_COLORING_SUBTITLE, - value: settings.autoColor.value, - onChanged: (isTrue) async { + value: settings.autoColor.valueR, + onChanged: (isTrue) { settings.save(autoColor: !isTrue); if (isTrue) { CurrentColor.inst.updatePlayerColorFromColor(playerStaticColor); @@ -220,11 +222,11 @@ class ThemeSetting extends SettingSubpageProvider { child: Obx( () => CustomSwitchListTile( bgColor: getBgColor(_ThemeSettingsKeys.wallpaperColors), - enabled: settings.autoColor.value, + enabled: settings.autoColor.valueR, icon: Broken.gallery_import, title: lang.PICK_COLORS_FROM_DEVICE_WALLPAPER, - value: settings.pickColorsFromDeviceWallpaper.value, - onChanged: (isTrue) async { + value: settings.pickColorsFromDeviceWallpaper.valueR, + onChanged: (isTrue) { settings.save(pickColorsFromDeviceWallpaper: !isTrue); _refreshColorCurrentPlayingItem(); }, @@ -239,8 +241,8 @@ class ThemeSetting extends SettingSubpageProvider { icon: Broken.slider_horizontal, title: lang.FORCE_MINIPLAYER_FOLLOW_TRACK_COLORS, subtitle: '${lang.IGNORES}: ${lang.AUTO_COLORING}, ${lang.PICK_COLORS_FROM_DEVICE_WALLPAPER} & ${lang.DEFAULT_COLOR}', - value: settings.forceMiniplayerTrackColor.value, - onChanged: (isTrue) async { + value: settings.forceMiniplayerTrackColor.valueR, + onChanged: (isTrue) { settings.save(forceMiniplayerTrackColor: !isTrue); _refreshColorCurrentPlayingItem(); }, @@ -249,16 +251,17 @@ class ThemeSetting extends SettingSubpageProvider { ), getItemWrapper( key: _ThemeSettingsKeys.pitchBlack, - child: Obx( - () => CustomSwitchListTile( + child: ObxO( + rx: settings.pitchBlack, + builder: (pitchBlack) => CustomSwitchListTile( bgColor: getBgColor(_ThemeSettingsKeys.pitchBlack), icon: Broken.mirror, title: lang.USE_PITCH_BLACK, subtitle: lang.USE_PITCH_BLACK_SUBTITLE, - value: settings.pitchBlack.value, - onChanged: (isTrue) async { + value: pitchBlack, + onChanged: (isTrue) { settings.save(pitchBlack: !isTrue); - _refreshColorCurrentPlayingItem(); + if (context.isDarkMode) _refreshColorCurrentPlayingItem(); }, ), ), @@ -351,14 +354,14 @@ class ThemeSetting extends SettingSubpageProvider { void _updateColorLight(Color color) { settings.save(staticColor: color.value); - if (!Get.isDarkMode) { + if (!namida.isDarkMode) { CurrentColor.inst.updatePlayerColorFromColor(color, false); } } void _updateColorDark(Color color) { settings.save(staticColorDark: color.value); - if (Get.isDarkMode) { + if (namida.isDarkMode) { CurrentColor.inst.updatePlayerColorFromColor(color, false); } } @@ -380,7 +383,7 @@ class ToggleThemeModeContainer extends StatelessWidget { final double containerWidth = width ?? context.width / 2.8; return Obx( () { - final currentTheme = settings.themeMode.value; + final currentTheme = settings.themeMode.valueR; return Container( decoration: BoxDecoration( color: Color.alphaBlend(context.theme.listTileTheme.textColor!.withAlpha(200), Colors.white.withAlpha(160)), @@ -404,7 +407,7 @@ class ToggleThemeModeContainer extends StatelessWidget { child: Container( width: containerWidth / 3.3, decoration: BoxDecoration( - color: context.theme.colorScheme.background.withAlpha(180), + color: context.theme.colorScheme.surface.withAlpha(180), borderRadius: BorderRadius.circular(8.0.multipliedRadius), // boxShadow: [ // BoxShadow(color: Colors.black.withAlpha(100), spreadRadius: 1, blurRadius: 4, offset: Offset(0, 2)), @@ -423,7 +426,7 @@ class ToggleThemeModeContainer extends StatelessWidget { onTap: () => onThemeChangeTap(e), child: Icon( e.toIcon(), - color: currentTheme == e ? context.theme.listTileTheme.iconColor : context.theme.colorScheme.background.withAlpha(180), + color: currentTheme == e ? context.theme.listTileTheme.iconColor : context.theme.colorScheme.surface.withAlpha(180), ), ), ), diff --git a/lib/ui/widgets/settings/youtube_settings.dart b/lib/ui/widgets/settings/youtube_settings.dart index f3280519..0e55a65f 100644 --- a/lib/ui/widgets/settings/youtube_settings.dart +++ b/lib/ui/widgets/settings/youtube_settings.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/base/setting_subpage_provider.dart'; import 'package:namida/controller/file_browser.dart'; @@ -10,6 +9,7 @@ import 'package:namida/core/enums.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/settings_card.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; @@ -62,10 +62,10 @@ class YoutubeSettings extends SettingSubpageProvider { bgColor: getBgColor(_YoutubeSettingKeys.youtubeStyleMiniplayer), icon: Broken.video_octagon, title: lang.YOUTUBE_STYLE_MINIPLAYER, - value: settings.youtubeStyleMiniplayer.value, + value: settings.youtubeStyleMiniplayer.valueR, onChanged: (isTrue) { settings.save(youtubeStyleMiniplayer: !isTrue); - Player.inst.tryGenerateWaveform(Player.inst.nowPlayingVideoID); + Player.inst.tryGenerateWaveform(Player.inst.currentVideo); }, ), ), @@ -77,7 +77,7 @@ class YoutubeSettings extends SettingSubpageProvider { bgColor: getBgColor(_YoutubeSettingKeys.rememberAudioOnly), icon: Broken.musicnote, title: lang.REMEMBER_AUDIO_ONLY_MODE, - value: settings.ytRememberAudioOnly.value, + value: settings.ytRememberAudioOnly.valueR, onChanged: (isTrue) => settings.save(ytRememberAudioOnly: !isTrue), ), ), @@ -94,14 +94,14 @@ class YoutubeSettings extends SettingSubpageProvider { ), title: lang.TOP_COMMENTS, subtitle: lang.TOP_COMMENTS_SUBTITLE, - value: settings.ytTopComments.value, + value: settings.ytTopComments.valueR, onChanged: (isTrue) { settings.save(ytTopComments: !isTrue); YoutubeController.inst.resetGlowUnderVideo(); // -- pop comments subpage in case was inside. if (settings.ytTopComments.value == false) { - NamidaNavigator.inst.ytMiniplayerCommentsPageKey?.currentState?.pop(); + NamidaNavigator.inst.ytMiniplayerCommentsPageKey.currentState?.pop(); NamidaNavigator.inst.isInYTCommentsSubpage = false; } }, @@ -120,15 +120,16 @@ class YoutubeSettings extends SettingSubpageProvider { ), title: lang.YT_PREFER_NEW_COMMENTS, subtitle: lang.YT_PREFER_NEW_COMMENTS_SUBTITLE, - value: settings.ytPreferNewComments.value, + value: settings.ytPreferNewComments.valueR, onChanged: (isTrue) => settings.save(ytPreferNewComments: !isTrue), ), ), ), getItemWrapper( key: _YoutubeSettingKeys.dimMiniplayerAfter, - child: Obx( - () => CustomListTile( + child: ObxO( + rx: settings.ytMiniplayerDimAfterSeconds, + builder: (ytMiniplayerDimAfterSeconds) => CustomListTile( bgColor: getBgColor(_YoutubeSettingKeys.dimMiniplayerAfter), leading: const StackedIcon( baseIcon: Broken.moon, @@ -137,12 +138,12 @@ class YoutubeSettings extends SettingSubpageProvider { ), title: lang.DIM_MINIPLAYER_AFTER_SECONDS.replaceFirst( '_SECONDS_', - "${settings.ytMiniplayerDimAfterSeconds.value}", + "$ytMiniplayerDimAfterSeconds", ), trailing: NamidaWheelSlider( totalCount: 120, - initValue: settings.ytMiniplayerDimAfterSeconds.value, - text: "${settings.ytMiniplayerDimAfterSeconds.value}s", + initValue: ytMiniplayerDimAfterSeconds, + text: "${ytMiniplayerDimAfterSeconds}s", onValueChanged: (val) { settings.save(ytMiniplayerDimAfterSeconds: val); }, @@ -155,7 +156,7 @@ class YoutubeSettings extends SettingSubpageProvider { child: Obx( () => CustomListTile( bgColor: getBgColor(_YoutubeSettingKeys.dimIntensity), - enabled: settings.ytMiniplayerDimAfterSeconds.value > 0, + enabled: settings.ytMiniplayerDimAfterSeconds.valueR > 0, leading: Stack( alignment: Alignment.center, children: [ @@ -187,7 +188,7 @@ class YoutubeSettings extends SettingSubpageProvider { const origin = height / 2; return Transform.rotate( origin: const Offset(0, origin), - angle: (settings.ytMiniplayerDimOpacity.value * multiplier) - minus, + angle: (settings.ytMiniplayerDimOpacity.valueR * multiplier) - minus, child: Container( width: 2.0, height: height, @@ -204,8 +205,8 @@ class YoutubeSettings extends SettingSubpageProvider { title: lang.DIM_INTENSITY, trailing: NamidaWheelSlider( totalCount: 100, - initValue: (settings.ytMiniplayerDimOpacity.value * 100).round(), - text: "${(settings.ytMiniplayerDimOpacity.value * 100).round()}%", + initValue: (settings.ytMiniplayerDimOpacity.valueR * 100).round(), + text: "${(settings.ytMiniplayerDimOpacity.valueR * 100).round()}%", onValueChanged: (val) { settings.save(ytMiniplayerDimOpacity: val / 100); }, @@ -239,7 +240,7 @@ class YoutubeSettings extends SettingSubpageProvider { .toList(), child: Obx( () => Text( - settings.ytTapToSeek.value.toText(), + settings.ytTapToSeek.valueR.toText(), textAlign: TextAlign.end, ), ), @@ -262,7 +263,7 @@ class YoutubeSettings extends SettingSubpageProvider { .toList(), child: Obx( () => Text( - settings.ytDragToSeek.value.toText(), + settings.ytDragToSeek.valueR.toText(), textAlign: TextAlign.end, ), ), @@ -283,7 +284,7 @@ class YoutubeSettings extends SettingSubpageProvider { ), title: lang.DOWNLOADS_METADATA_TAGS, subtitle: lang.DOWNLOADS_METADATA_TAGS_SUBTITLE, - value: settings.ytAutoExtractVideoTagsFromInfo.value, + value: settings.ytAutoExtractVideoTagsFromInfo.valueR, onChanged: (isTrue) => settings.save(ytAutoExtractVideoTagsFromInfo: !isTrue), ), ), @@ -295,7 +296,7 @@ class YoutubeSettings extends SettingSubpageProvider { bgColor: getBgColor(_YoutubeSettingKeys.downloadLocation), title: lang.DEFAULT_DOWNLOAD_LOCATION, icon: Broken.folder_favorite, - subtitle: settings.ytDownloadLocation.value, + subtitle: settings.ytDownloadLocation.valueR, onTap: () async { final path = await NamidaFileBrowser.getDirectory(note: lang.DEFAULT_DOWNLOAD_LOCATION); if (path != null) settings.save(ytDownloadLocation: path); @@ -310,7 +311,7 @@ class YoutubeSettings extends SettingSubpageProvider { bgColor: getBgColor(_YoutubeSettingKeys.onOpeningYTLink), icon: Broken.import_1, title: lang.ON_OPENING_YOUTUBE_LINK, - trailingText: settings.onYoutubeLinkOpen.value.toText(), + trailingText: settings.onYoutubeLinkOpen.valueR.toText(), onTap: () { NamidaNavigator.inst.navigateDialog( dialog: CustomBlurryDialog( @@ -326,11 +327,12 @@ class YoutubeSettings extends SettingSubpageProvider { ...OnYoutubeLinkOpenAction.values.map( (e) => Padding( padding: const EdgeInsets.all(6.0), - child: Obx( - () => ListTileWithCheckMark( + child: ObxO( + rx: settings.onYoutubeLinkOpen, + builder: (onYoutubeLinkOpen) => ListTileWithCheckMark( icon: e.toIcon(), title: e.toText(), - active: settings.onYoutubeLinkOpen.value == e, + active: onYoutubeLinkOpen == e, onTap: () { settings.save(onYoutubeLinkOpen: e); }, diff --git a/lib/ui/widgets/settings_card.dart b/lib/ui/widgets/settings_card.dart index f9181d03..255c1bdf 100644 --- a/lib/ui/widgets/settings_card.dart +++ b/lib/ui/widgets/settings_card.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - import 'package:namida/core/extensions.dart'; +import 'package:namida/core/utils.dart'; class SettingsCard extends StatelessWidget { final Widget child; @@ -62,7 +61,7 @@ class SettingsCard extends StatelessWidget { children: [ Text( title, - style: context.textTheme.displayLarge?.copyWith(fontSize: 18.0.multipliedFontScale), + style: context.textTheme.displayLarge?.copyWith(fontSize: 18.0), ), if (subtitle != null) Text(subtitle!, style: context.textTheme.displaySmall), ], diff --git a/lib/ui/widgets/settings_search_bar.dart b/lib/ui/widgets/settings_search_bar.dart index 730b6fdf..4beb9760 100644 --- a/lib/ui/widgets/settings_search_bar.dart +++ b/lib/ui/widgets/settings_search_bar.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/controller/settings_search_controller.dart'; -import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/packages/searchbar_animation.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; @@ -61,12 +60,12 @@ class _NamidaSettingSearchBarState extends State { enableBoxShadow: false, buttonShadowColour: Colors.transparent, hintTextStyle: (height) => context.textTheme.displaySmall?.copyWith( - fontSize: 17.0.multipliedFontScale, + fontSize: 17.0, height: height * 1.1, ), searchBoxColour: context.theme.cardColor.withAlpha(200), enteredTextStyle: context.theme.textTheme.displayMedium, - cursorColour: context.theme.colorScheme.onBackground, + cursorColour: context.theme.colorScheme.onSurface, buttonBorderColour: Colors.black45, cursorRadius: const Radius.circular(12.0), buttonWidget: const IgnorePointer( diff --git a/lib/ui/widgets/sort_by_button.dart b/lib/ui/widgets/sort_by_button.dart index cb59e907..7554bd3c 100644 --- a/lib/ui/widgets/sort_by_button.dart +++ b/lib/ui/widgets/sort_by_button.dart @@ -1,13 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - import 'package:namida/controller/search_sort_controller.dart'; import 'package:namida/controller/settings_controller.dart'; import 'package:namida/core/enums.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; class SortByMenuTracks extends StatelessWidget { @@ -15,47 +14,44 @@ class SortByMenuTracks extends StatelessWidget { @override Widget build(BuildContext context) { - return Obx( - () { - final tracksSort = settings.tracksSort.value; - final reversed = settings.tracksSortReversed.value; - return Column( - children: [ - ListTileWithCheckMark( - active: reversed, - onTap: () => SearchSortController.inst.sortMedia(MediaType.track, reverse: !reversed), - ), - ...[ - SortType.title, - SortType.album, - SortType.artistsList, - SortType.albumArtist, - SortType.composer, - SortType.genresList, - SortType.year, - SortType.dateAdded, - SortType.dateModified, - SortType.bitrate, - SortType.trackNo, - SortType.discNo, - SortType.filename, - SortType.duration, - SortType.sampleRate, - SortType.size, - SortType.rating, - SortType.latestPlayed, - SortType.mostPlayed, - SortType.shuffle, - ].map( - (e) => SmallListTile( - title: e.toText(), - active: tracksSort == e, - onTap: () => SearchSortController.inst.sortMedia(MediaType.track, sortBy: e), - ), + return Column( + children: [ + ListTileWithCheckMark( + activeRx: settings.tracksSortReversed, + onTap: () => SearchSortController.inst.sortMedia(MediaType.track, reverse: !settings.tracksSortReversed.value), + ), + ...[ + SortType.title, + SortType.album, + SortType.artistsList, + SortType.albumArtist, + SortType.composer, + SortType.genresList, + SortType.year, + SortType.dateAdded, + SortType.dateModified, + SortType.bitrate, + SortType.trackNo, + SortType.discNo, + SortType.filename, + SortType.duration, + SortType.sampleRate, + SortType.size, + SortType.rating, + SortType.latestPlayed, + SortType.mostPlayed, + SortType.shuffle, + ].map( + (e) => ObxO( + rx: settings.tracksSort, + builder: (tracksSort) => SmallListTile( + title: e.toText(), + active: tracksSort == e, + onTap: () => SearchSortController.inst.sortMedia(MediaType.track, sortBy: e), ), - ], - ); - }, + ), + ), + ], ); } } @@ -70,9 +66,9 @@ class SortByMenuTracksSearch extends StatelessWidget { child: SingleChildScrollView( child: Obx( () { - final tracksSortSearch = settings.tracksSortSearch.value; - final reversed = settings.tracksSortSearchReversed.value; - final isAuto = settings.tracksSortSearchIsAuto.value; + final tracksSortSearch = settings.tracksSortSearch.valueR; + final reversed = settings.tracksSortSearchReversed.valueR; + final isAuto = settings.tracksSortSearchIsAuto.valueR; return Column( children: [ Padding( @@ -80,9 +76,9 @@ class SortByMenuTracksSearch extends StatelessWidget { child: ListTileWithCheckMark( icon: Broken.arrow_swap_horizontal, title: lang.AUTO, - active: isAuto, + activeRx: settings.tracksSortSearchIsAuto, onTap: () { - settings.save(tracksSortSearchIsAuto: !isAuto); + settings.save(tracksSortSearchIsAuto: !settings.tracksSortSearchIsAuto.value); SearchSortController.inst.sortTracksSearch(canSkipSorting: false); }, ), @@ -102,7 +98,7 @@ class SortByMenuTracksSearch extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), child: ListTileWithCheckMark( - active: isAuto ? settings.tracksSortReversed.value : reversed, + active: isAuto ? settings.tracksSortReversed.valueR : reversed, onTap: () { SearchSortController.inst.sortTracksSearch(reverse: !reversed); }, @@ -158,35 +154,33 @@ class SortByMenuAlbums extends StatelessWidget { @override Widget build(BuildContext context) { - return Obx( - () { - final albumsort = settings.albumSort.value; - return Column( - children: [ - ListTileWithCheckMark( - active: settings.albumSortReversed.value, - onTap: () => SearchSortController.inst.sortMedia(MediaType.album, reverse: !settings.albumSortReversed.value), - ), - ...[ - GroupSortType.album, - GroupSortType.albumArtist, - GroupSortType.year, - GroupSortType.duration, - GroupSortType.numberOfTracks, - GroupSortType.dateModified, - GroupSortType.artistsList, - GroupSortType.composer, - GroupSortType.shuffle, - ].map( - (e) => SmallListTile( - title: e.toText(), - active: albumsort == e, - onTap: () => SearchSortController.inst.sortMedia(MediaType.album, groupSortBy: e), - ), + return Column( + children: [ + ListTileWithCheckMark( + activeRx: settings.albumSortReversed, + onTap: () => SearchSortController.inst.sortMedia(MediaType.album, reverse: !settings.albumSortReversed.value), + ), + ...[ + GroupSortType.album, + GroupSortType.albumArtist, + GroupSortType.year, + GroupSortType.duration, + GroupSortType.numberOfTracks, + GroupSortType.dateModified, + GroupSortType.artistsList, + GroupSortType.composer, + GroupSortType.shuffle, + ].map( + (e) => ObxO( + rx: settings.albumSort, + builder: (albumsort) => SmallListTile( + title: e.toText(), + active: albumsort == e, + onTap: () => SearchSortController.inst.sortMedia(MediaType.album, groupSortBy: e), ), - ], - ); - }, + ), + ), + ], ); } } @@ -196,40 +190,38 @@ class SortByMenuArtists extends StatelessWidget { @override Widget build(BuildContext context) { - return Obx( - () { - final artistSort = settings.artistSort.value; - final artistType = settings.activeArtistType.value; - return Column( - children: [ - ListTileWithCheckMark( - active: settings.artistSortReversed.value, - onTap: () => SearchSortController.inst.sortMedia(MediaType.artist, reverse: !settings.artistSortReversed.value), - ), - ...[ - artistType == MediaType.albumArtist - ? GroupSortType.albumArtist - : artistType == MediaType.composer - ? GroupSortType.composer - : GroupSortType.artistsList, - GroupSortType.numberOfTracks, - GroupSortType.albumsCount, - GroupSortType.duration, - GroupSortType.genresList, - GroupSortType.album, - GroupSortType.year, - GroupSortType.dateModified, - GroupSortType.shuffle, - ].map( - (e) => SmallListTile( - title: e.toText(), - active: artistSort == e, - onTap: () => SearchSortController.inst.sortMedia(MediaType.artist, groupSortBy: e), - ), + final artistType = settings.activeArtistType.value; + return Column( + children: [ + ListTileWithCheckMark( + activeRx: settings.artistSortReversed, + onTap: () => SearchSortController.inst.sortMedia(MediaType.artist, reverse: !settings.artistSortReversed.value), + ), + ...[ + artistType == MediaType.albumArtist + ? GroupSortType.albumArtist + : artistType == MediaType.composer + ? GroupSortType.composer + : GroupSortType.artistsList, + GroupSortType.numberOfTracks, + GroupSortType.albumsCount, + GroupSortType.duration, + GroupSortType.genresList, + GroupSortType.album, + GroupSortType.year, + GroupSortType.dateModified, + GroupSortType.shuffle, + ].map( + (e) => ObxO( + rx: settings.artistSort, + builder: (artistSort) => SmallListTile( + title: e.toText(), + active: artistSort == e, + onTap: () => SearchSortController.inst.sortMedia(MediaType.artist, groupSortBy: e), ), - ], - ); - }, + ), + ), + ], ); } } @@ -239,36 +231,34 @@ class SortByMenuGenres extends StatelessWidget { @override Widget build(BuildContext context) { - return Obx( - () { - final genreSort = settings.genreSort.value; - return Column( - children: [ - ListTileWithCheckMark( - active: settings.genreSortReversed.value, - onTap: () => SearchSortController.inst.sortMedia(MediaType.genre, reverse: !settings.genreSortReversed.value), - ), - ...[ - GroupSortType.genresList, - GroupSortType.duration, - GroupSortType.numberOfTracks, - GroupSortType.year, - GroupSortType.artistsList, - GroupSortType.album, - GroupSortType.albumArtist, - GroupSortType.dateModified, - GroupSortType.composer, - GroupSortType.shuffle, - ].map( - (e) => SmallListTile( - title: e.toText(), - active: genreSort == e, - onTap: () => SearchSortController.inst.sortMedia(MediaType.genre, groupSortBy: e), - ), + return Column( + children: [ + ListTileWithCheckMark( + activeRx: settings.genreSortReversed, + onTap: () => SearchSortController.inst.sortMedia(MediaType.genre, reverse: !settings.genreSortReversed.value), + ), + ...[ + GroupSortType.genresList, + GroupSortType.duration, + GroupSortType.numberOfTracks, + GroupSortType.year, + GroupSortType.artistsList, + GroupSortType.album, + GroupSortType.albumArtist, + GroupSortType.dateModified, + GroupSortType.composer, + GroupSortType.shuffle, + ].map( + (e) => ObxO( + rx: settings.genreSort, + builder: (genreSort) => SmallListTile( + title: e.toText(), + active: genreSort == e, + onTap: () => SearchSortController.inst.sortMedia(MediaType.genre, groupSortBy: e), ), - ], - ); - }, + ), + ), + ], ); } } @@ -278,32 +268,30 @@ class SortByMenuPlaylist extends StatelessWidget { @override Widget build(BuildContext context) { - return Obx( - () { - final playlistSort = settings.playlistSort.value; - return Column( - children: [ - ListTileWithCheckMark( - active: settings.playlistSortReversed.value, - onTap: () => SearchSortController.inst.sortMedia(MediaType.playlist, reverse: !settings.playlistSortReversed.value), - ), - ...[ - GroupSortType.title, - GroupSortType.creationDate, - GroupSortType.modifiedDate, - GroupSortType.duration, - GroupSortType.numberOfTracks, - GroupSortType.shuffle, - ].map( - (e) => SmallListTile( - title: e.toText(), - active: playlistSort == e, - onTap: () => SearchSortController.inst.sortMedia(MediaType.playlist, groupSortBy: e), - ), + return Column( + children: [ + ListTileWithCheckMark( + activeRx: settings.playlistSortReversed, + onTap: () => SearchSortController.inst.sortMedia(MediaType.playlist, reverse: !settings.playlistSortReversed.value), + ), + ...[ + GroupSortType.title, + GroupSortType.creationDate, + GroupSortType.modifiedDate, + GroupSortType.duration, + GroupSortType.numberOfTracks, + GroupSortType.shuffle, + ].map( + (e) => ObxO( + rx: settings.playlistSort, + builder: (playlistSort) => SmallListTile( + title: e.toText(), + active: playlistSort == e, + onTap: () => SearchSortController.inst.sortMedia(MediaType.playlist, groupSortBy: e), ), - ], - ); - }, + ), + ), + ], ); } } diff --git a/lib/ui/widgets/stats.dart b/lib/ui/widgets/stats.dart index 6c4ff70c..a7bdb0d4 100644 --- a/lib/ui/widgets/stats.dart +++ b/lib/ui/widgets/stats.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/controller/indexer_controller.dart'; import 'package:namida/controller/player_controller.dart'; @@ -7,6 +6,7 @@ import 'package:namida/core/constants.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/settings_card.dart'; @@ -22,63 +22,66 @@ class StatsSection extends StatelessWidget { child: SizedBox( width: context.width, child: Obx( - () => Wrap( - alignment: WrapAlignment.start, - children: [ - StatsContainer( - icon: Broken.music_circle, - title: '${lang.TRACKS} :', - value: allTracksInLibrary.length.formatDecimal(), - ), - StatsContainer( - icon: Broken.music_dashboard, - title: '${lang.ALBUMS} :', - value: Indexer.inst.mainMapAlbums.value.keys.length.formatDecimal(), - ), - StatsContainer( - icon: Broken.microphone, - title: '${lang.ARTISTS} :', - value: Indexer.inst.mainMapArtists.value.length.formatDecimal(), - ), - StatsContainer( - icon: Broken.smileys, - title: '${lang.GENRES} :', - value: Indexer.inst.mainMapGenres.value.length.formatDecimal(), - ), - StatsContainer( - icon: Broken.music_library_2, - title: '${lang.TOTAL_TRACKS_DURATION} :', - value: allTracksInLibrary.totalDurationFormatted, - ), - Obx( - () { - final map = Player.inst.totalListenedTimeInSec; - final trSec = map?[LibraryCategory.localTracks] ?? 0; - return StatsContainer( - icon: Broken.timer_1, - title: '${lang.TOTAL_LISTEN_TIME} :', - value: trSec.formattedTime, - ); - }, - ), - Obx( - () { - final map = Player.inst.totalListenedTimeInSec; - final sec = map?[LibraryCategory.youtube] ?? 0; - return StatsContainer( - leading: const StackedIcon( - baseIcon: Broken.timer_1, - secondaryIcon: Broken.video_square, - secondaryIconSize: 12.0, - ), - icon: Broken.timer_1, - title: '${lang.TOTAL_LISTEN_TIME} (${lang.YOUTUBE}) :', - value: sec.formattedTime, - ); - }, - ), - ], - ), + () { + final allTracks = Indexer.inst.tracksInfoList.valueR; + return Wrap( + alignment: WrapAlignment.start, + children: [ + StatsContainer( + icon: Broken.music_circle, + title: '${lang.TRACKS} :', + value: allTracks.length.formatDecimal(), + ), + StatsContainer( + icon: Broken.music_dashboard, + title: '${lang.ALBUMS} :', + value: Indexer.inst.mainMapAlbums.valueR.keys.length.formatDecimal(), + ), + StatsContainer( + icon: Broken.microphone, + title: '${lang.ARTISTS} :', + value: Indexer.inst.mainMapArtists.valueR.length.formatDecimal(), + ), + StatsContainer( + icon: Broken.smileys, + title: '${lang.GENRES} :', + value: Indexer.inst.mainMapGenres.valueR.length.formatDecimal(), + ), + StatsContainer( + icon: Broken.music_library_2, + title: '${lang.TOTAL_TRACKS_DURATION} :', + value: allTracks.totalDurationFormatted, + ), + Obx( + () { + final map = Player.inst.totalListenedTimeInSec; + final trSec = map?[LibraryCategory.localTracks] ?? 0; + return StatsContainer( + icon: Broken.timer_1, + title: '${lang.TOTAL_LISTEN_TIME} :', + value: trSec.formattedTime, + ); + }, + ), + Obx( + () { + final map = Player.inst.totalListenedTimeInSec; + final sec = map?[LibraryCategory.youtube] ?? 0; + return StatsContainer( + leading: const StackedIcon( + baseIcon: Broken.timer_1, + secondaryIcon: Broken.video_square, + secondaryIconSize: 12.0, + ), + icon: Broken.timer_1, + title: '${lang.TOTAL_LISTEN_TIME} (${lang.YOUTUBE}) :', + value: sec.formattedTime, + ); + }, + ), + ], + ); + }, ), ), ); diff --git a/lib/ui/widgets/video_widget.dart b/lib/ui/widgets/video_widget.dart index 7f7e19b3..a529ab2a 100644 --- a/lib/ui/widgets/video_widget.dart +++ b/lib/ui/widgets/video_widget.dart @@ -5,7 +5,6 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart'; -import 'package:get/get.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; import 'package:namida/class/track.dart'; @@ -20,6 +19,7 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/packages/three_arched_circle.dart'; import 'package:namida/ui/dialogs/edit_tags_dialog.dart'; import 'package:namida/ui/widgets/artwork.dart'; @@ -91,8 +91,6 @@ class NamidaVideoControlsState extends State with TickerPro _startTimer(); } - final userSeekMS = 0.obs; - Widget _getBuilder({ required Widget child, }) { @@ -150,7 +148,7 @@ class NamidaVideoControlsState extends State with TickerPro onSecondsReady: (finalSeconds) { if (_shouldSeekOnTap && !_lastSeekWasForward) { // only increase if not at the start - if (Player.inst.nowPlayingPosition != 0) { + if (Player.inst.nowPlayingPosition.value != 0) { _seekSeconds += finalSeconds; } } else { @@ -167,7 +165,7 @@ class NamidaVideoControlsState extends State with TickerPro onSecondsReady: (finalSeconds) { if (_shouldSeekOnTap && _lastSeekWasForward) { // only increase if not at the end - if (Player.inst.nowPlayingPosition != Player.inst.currentItemDuration?.inMilliseconds) { + if (Player.inst.nowPlayingPosition.value != Player.inst.currentItemDuration.value?.inMilliseconds) { _seekSeconds += finalSeconds; } } else { @@ -260,7 +258,6 @@ class NamidaVideoControlsState extends State with TickerPro seekAnimationForward2.dispose(); seekAnimationBackward1.dispose(); seekAnimationBackward2.dispose(); - userSeekMS.close(); _currentDeviceVolume.close(); _canShowBrightnessSlider.close(); Player.inst.onVolumeChangeRemoveListener(_volumeListenerKey); @@ -393,12 +390,12 @@ class NamidaVideoControlsState extends State with TickerPro const SizedBox(width: 4.0), Text( title, - style: context.textTheme.displayMedium?.copyWith(fontSize: 13.0.multipliedFontScale), + style: context.textTheme.displayMedium?.copyWith(fontSize: 13.0), ), if (subtitle != '') Text( subtitle, - style: context.textTheme.displaySmall?.copyWith(fontSize: 12.0.multipliedFontScale), + style: context.textTheme.displaySmall?.copyWith(fontSize: 12.0), ), ], ), @@ -444,7 +441,7 @@ class NamidaVideoControlsState extends State with TickerPro bool _isDraggingSeekBar = false; - RxDouble get _currentBrigthnessDim => VideoController.inst.currentBrigthnessDim; + Rx get _currentBrigthnessDim => VideoController.inst.currentBrigthnessDim; Widget _getVerticalSliderWidget(String key, double? perc, IconData icon, ui.FlutterView view) { final totalHeight = view.physicalSize.shortestSide / view.devicePixelRatio * 0.75; @@ -516,7 +513,6 @@ class NamidaVideoControlsState extends State with TickerPro bool get _canShowControls => widget.showControls && !NamidaChannel.inst.isInPip.value; EdgeInsets _deviceInsets = EdgeInsets.zero; - Orientation? _latestOrientation; @override Widget build(BuildContext context) { @@ -528,57 +524,68 @@ class NamidaVideoControlsState extends State with TickerPro final fallbackHeight = inLandscape ? maxHeight : maxWidth * 9 / 16; final fallbackWidth = inLandscape ? maxHeight * 16 / 9 : maxWidth; - final finalVideoWidget = Obx(() { - final info = Player.inst.videoPlayerInfo; - if (info != null && info.isInitialized) { - return NamidaAspectRatio( - aspectRatio: info.aspectRatio, - child: Obx( - () => AnimatedScale( - duration: const Duration(milliseconds: 200), - scale: 1.0 + VideoController.inst.videoZoomAdditionalScale.value * 0.02, - child: Texture(textureId: info.textureId), - ), - ), - ); - } - if (widget.isLocal && !widget.isFullScreen) { - return Container( - key: const Key('dummy_container'), - color: Colors.transparent, - ); - } - // -- fallback images - if (widget.isLocal) { - final track = Player.inst.nowPlayingTrack; - if (File(track.pathToImage).existsSync()) { - return ArtworkWidget( - key: Key(track.path), - track: track, - path: track.pathToImage, - thumbnailSize: fallbackWidth, - width: fallbackWidth, - height: fallbackHeight, - borderRadius: 0, - blur: 0, - compressed: false, + final finalVideoWidget = ObxO( + rx: Player.inst.videoPlayerInfo, + builder: (info) { + if (info != null && info.isInitialized) { + return NamidaAspectRatio( + aspectRatio: info.aspectRatio, + child: ObxO( + rx: VideoController.inst.videoZoomAdditionalScale, + builder: (pinchInZoom) => AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: 1.0 + pinchInZoom * 0.02, + child: Texture(textureId: info.textureId), + ), + ), + ); + } + if (widget.isLocal && !widget.isFullScreen) { + return Container( + key: const Key('dummy_container'), + color: Colors.transparent, + ); + } + // -- fallback images + if (widget.isLocal) { + return ObxO( + rx: Player.inst.currentItem, + builder: (item) { + final track = item is Selectable ? item.track : null; + return ArtworkWidget( + key: ValueKey(track?.path), + track: track, + path: track?.pathToImage, + thumbnailSize: fallbackWidth, + width: fallbackWidth, + height: fallbackHeight, + borderRadius: 0, + blur: 0, + compressed: false, + ); + }); + } + return Obx( + () { + final vidId = Player.inst.currentVideoR?.id ?? (YoutubeController.inst.currentYoutubeMetadataVideo.valueR ?? Player.inst.currentVideoInfo.valueR)?.id; + return YoutubeThumbnail( + key: Key(vidId ?? ''), + isImportantInCache: true, + width: fallbackWidth, + height: fallbackHeight, + borderRadius: 0, + blur: 0, + videoId: vidId, + displayFallbackIcon: false, + compressed: false, + preferLowerRes: false, + ); + }, ); - } - } - final vidId = Player.inst.nowPlayingVideoID?.id ?? (YoutubeController.inst.currentYoutubeMetadataVideo.value ?? Player.inst.currentVideoInfo)?.id; - return YoutubeThumbnail( - key: Key(vidId ?? ''), - isImportantInCache: true, - width: fallbackWidth, - height: fallbackHeight, - borderRadius: 0, - blur: 0, - videoId: vidId, - displayFallbackIcon: false, - compressed: false, - preferLowerRes: false, - ); - }); + }); + + final newDeviceInsets = MediaQuery.paddingOf(context); + if (newDeviceInsets != EdgeInsets.zero) _deviceInsets = newDeviceInsets; final horizontalControlsPadding = widget.isFullScreen ? inLandscape @@ -599,12 +606,6 @@ class NamidaVideoControlsState extends State with TickerPro final shouldShowSliders = _canShowControls && widget.isFullScreen; final shouldShowSeekBar = widget.isFullScreen; final view = View.of(context); - final newDeviceInsets = MediaQuery.paddingOf(context); - final newOrientation = MediaQuery.orientationOf(context); - if (_latestOrientation != newOrientation || _deviceInsets.horizontal < newDeviceInsets.horizontal || _deviceInsets.vertical < newDeviceInsets.vertical) { - _deviceInsets = newDeviceInsets; - _latestOrientation = newOrientation; - } return Listener( onPointerDown: (event) { @@ -689,9 +690,10 @@ class NamidaVideoControlsState extends State with TickerPro ), // ---- Brightness Mask ----- Positioned.fill( - child: Obx( - () => Container( - color: Colors.black.withOpacity(1 - _currentBrigthnessDim.value), + child: ObxO( + rx: _currentBrigthnessDim, + builder: (brightness) => Container( + color: Colors.black.withOpacity(1 - brightness), ), ), ), @@ -732,11 +734,11 @@ class NamidaVideoControlsState extends State with TickerPro type: MaterialType.transparency, child: Obx(() { final videoName = widget.isLocal - ? Player.inst.nowPlayingTrack.title - : YoutubeController.inst.currentYoutubeMetadataVideo.value?.name ?? Player.inst.currentVideoInfo?.name ?? ''; + ? Player.inst.currentTrackR?.track.title ?? '' + : YoutubeController.inst.currentYoutubeMetadataVideo.valueR?.name ?? Player.inst.currentVideoInfo.valueR?.name ?? ''; final channelName = widget.isLocal - ? Player.inst.nowPlayingTrack.originalArtist - : YoutubeController.inst.currentYoutubeMetadataChannel.value?.name ?? Player.inst.currentVideoInfo?.uploaderName ?? ''; + ? Player.inst.currentTrackR?.track.originalArtist ?? '' + : YoutubeController.inst.currentYoutubeMetadataChannel.valueR?.name ?? Player.inst.currentVideoInfo.valueR?.uploaderName ?? ''; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -759,10 +761,11 @@ class NamidaVideoControlsState extends State with TickerPro ), const SizedBox(width: 8.0), // ==== Reset Brightness ==== - Obx( - () => AnimatedSwitcher( + ObxO( + rx: _currentBrigthnessDim, + builder: (brigthnessDim) => AnimatedSwitcher( duration: const Duration(milliseconds: 200), - child: _currentBrigthnessDim.value < 1.0 + child: brigthnessDim < 1.0 ? NamidaIconButton( key: const Key('brightnesseto_ok'), tooltip: lang.RESET_BRIGHTNESS, @@ -787,9 +790,10 @@ class NamidaVideoControlsState extends State with TickerPro }, children: () => [ ...settings.player.speeds.map((speed) { - return Obx( - () { - final isSelected = Player.inst.currentSpeed == speed; + return ObxO( + rx: Player.inst.currentSpeed, + builder: (selectedSpeed) { + final isSelected = selectedSpeed == speed; return NamidaInkWell( onTap: () { _startTimer(); @@ -810,14 +814,14 @@ class NamidaVideoControlsState extends State with TickerPro const SizedBox(width: 12.0), Text( "${speed}x", - style: context.textTheme.displayMedium?.copyWith(fontSize: 13.0.multipliedFontScale), + style: context.textTheme.displayMedium?.copyWith(fontSize: 13.0), ), ], ), ); }, ); - }).toList(), + }), NamidaInkWell( onTap: () { _startTimer(); @@ -835,7 +839,7 @@ class NamidaVideoControlsState extends State with TickerPro const SizedBox(width: 12.0), Text( lang.ADD, - style: context.textTheme.displayMedium?.copyWith(fontSize: 13.0.multipliedFontScale), + style: context.textTheme.displayMedium?.copyWith(fontSize: 13.0), ), ], ), @@ -854,23 +858,26 @@ class NamidaVideoControlsState extends State with TickerPro borderRadius: BorderRadius.circular(6.0.multipliedRadius), ), child: Obx( - () => Row( - children: [ - Icon( - Broken.play_cricle, - size: 16.0, - color: itemsColor, - ), - const SizedBox(width: 4.0).animateEntrance(showWhen: Player.inst.currentSpeed != 1.0, allCurves: Curves.easeInOutQuart), - Text( - "${Player.inst.currentSpeed}x", - style: context.textTheme.displaySmall?.copyWith( + () { + final speed = Player.inst.currentSpeed.valueR; + return Row( + children: [ + Icon( + Broken.play_cricle, + size: 16.0, color: itemsColor, - fontSize: 12.0, ), - ).animateEntrance(showWhen: Player.inst.currentSpeed != 1.0, allCurves: Curves.easeInOutQuart), - ], - ), + const SizedBox(width: 4.0).animateEntrance(showWhen: speed != 1.0, allCurves: Curves.easeInOutQuart), + Text( + "${speed}x", + style: context.textTheme.displaySmall?.copyWith( + color: itemsColor, + fontSize: 12.0, + ), + ).animateEntrance(showWhen: speed != 1.0, allCurves: Curves.easeInOutQuart), + ], + ); + }, ), ), ), @@ -878,10 +885,10 @@ class NamidaVideoControlsState extends State with TickerPro ), ), Obx(() { - final audioStreamsAll = List.from(YoutubeController.inst.currentYTAudioStreams); + final audioStreamsAll = List.from(YoutubeController.inst.currentYTAudioStreams.valueR); final streamsMap = {}; // {language: audiostream} audioStreamsAll.sortBy((e) => e.displayLanguage ?? ''); - audioStreamsAll.loop((e, index) { + audioStreamsAll.loop((e) { if (e.language != null && e.formatSuffix != 'webm') { streamsMap[e.language!] = e; } @@ -899,10 +906,10 @@ class NamidaVideoControlsState extends State with TickerPro ...streamsMap.values.map( (element) => Obx( () { - final isSelected1 = element.language == Player.inst.currentCachedAudio?.langaugeCode; - final isSelected2 = element.language == Player.inst.currentAudioStream?.language; + final isSelected1 = element.language == Player.inst.currentCachedAudio.valueR?.langaugeCode; + final isSelected2 = element.language == Player.inst.currentAudioStream.valueR?.language; final isSelected = isSelected1 || isSelected2; - final id = Player.inst.nowPlayingVideoID?.id; + final id = Player.inst.currentVideoR?.id; return _getQualityChip( title: '${element.displayLanguage}', subtitle: " • ${element.language ?? 0}", @@ -912,7 +919,7 @@ class NamidaVideoControlsState extends State with TickerPro stream: element, cachedFile: null, useCache: true, - videoId: Player.inst.nowPlayingVideoID?.id ?? '', + videoId: Player.inst.currentVideo?.id ?? '', ); } }, @@ -937,8 +944,8 @@ class NamidaVideoControlsState extends State with TickerPro ), child: Obx( () { - final currentStream = Player.inst.currentAudioStream; - final currentCached = Player.inst.currentCachedAudio; + final currentStream = Player.inst.currentAudioStream.valueR; + final currentCached = Player.inst.currentCachedAudio.valueR; final qt = currentStream?.displayLanguage ?? currentCached?.langaugeName; return qt == null ? const SizedBox() @@ -960,8 +967,8 @@ class NamidaVideoControlsState extends State with TickerPro final ytQualities = (widget.isLocal ? VideoController.inst.currentYTQualities : YoutubeController.inst.currentYTQualities).where((s) => s.formatSuffix != 'webm'); final cachedQualitiesAll = widget.isLocal ? VideoController.inst.currentPossibleVideos : YoutubeController.inst.currentCachedQualities; - final cachedQualities = List.from(cachedQualitiesAll); - final videoId = Player.inst.nowPlayingVideoID?.id; + final cachedQualities = List.from(cachedQualitiesAll.valueR); + final videoId = Player.inst.currentVideoR?.id; cachedQualities.removeWhere( (cq) { return ytQualities.any((ytq) { @@ -991,7 +998,7 @@ class NamidaVideoControlsState extends State with TickerPro VideoController.inst.currentVideo.value = null; settings.save(enableVideoPlayback: false); }, - selected: (widget.isLocal ? VideoController.inst.currentVideo.value == null : Player.inst.isAudioOnlyPlayback), + selected: (widget.isLocal ? VideoController.inst.currentVideo.valueR == null : settings.ytIsAudioOnlyMode.valueR), isCached: false, icon: Broken.musicnote, ), @@ -1003,13 +1010,13 @@ class NamidaVideoControlsState extends State with TickerPro subtitle: " • ${element.sizeInBytes.fileSizeFormatted}", onPlay: (isSelected) { // sometimes video is not initialized so we need the second check - if (!isSelected || !Player.inst.videoInitialized) { + if (!isSelected || Player.inst.videoPlayerInfo.value?.isInitialized != true) { Player.inst.onItemPlayYoutubeIDSetQuality( stream: null, cachedFile: File(element.path), videoItem: element, useCache: true, - videoId: Player.inst.nowPlayingVideoID?.id ?? '', + videoId: Player.inst.currentVideo?.id ?? '', ); if (widget.isLocal) { VideoController.inst.currentVideo.value = element; @@ -1018,10 +1025,10 @@ class NamidaVideoControlsState extends State with TickerPro } }, selected: widget.isLocal - ? VideoController.inst.currentVideo.value?.path == element.path - : Player.inst.isAudioOnlyPlayback + ? VideoController.inst.currentVideo.valueR?.path == element.path + : settings.ytIsAudioOnlyMode.valueR ? false - : Player.inst.currentCachedVideo?.path == element.path, + : Player.inst.currentCachedVideo.valueR?.path == element.path, isCached: true, ), ), @@ -1031,14 +1038,14 @@ class NamidaVideoControlsState extends State with TickerPro return Obx( () { if (widget.isLocal) { - final id = Player.inst.nowPlayingVideoID?.id; - final isSelected = element.height == VideoController.inst.currentVideo.value?.height; + final id = Player.inst.currentVideoR?.id; + final isSelected = element.height == VideoController.inst.currentVideo.valueR?.height; return _getQualityChip( title: element.resolution ?? '', subtitle: sizeInBytes == null ? '' : " • ${sizeInBytes.fileSizeFormatted}", onPlay: (isSelected) { - if (!isSelected || !Player.inst.videoInitialized) { + if (!isSelected || Player.inst.videoPlayerInfo.value?.isInitialized != true) { Player.inst.onItemPlayYoutubeIDSetQuality( stream: element, cachedFile: null, @@ -1051,12 +1058,12 @@ class NamidaVideoControlsState extends State with TickerPro isCached: isSelected, ); } else { - final id = Player.inst.nowPlayingVideoID?.id; + final id = Player.inst.currentVideoR?.id; final cachedFile = id == null ? null : element.getCachedFile(id); - final isSelected = Player.inst.isAudioOnlyPlayback + final isSelected = settings.ytIsAudioOnlyMode.valueR ? false - : (element.resolution == Player.inst.currentVideoStream?.resolution || - (Player.inst.currentCachedVideo != null && cachedFile?.path == Player.inst.currentCachedVideo?.path)); + : (element.resolution == Player.inst.currentVideoStream.valueR?.resolution || + (Player.inst.currentCachedVideo.valueR != null && cachedFile?.path == Player.inst.currentCachedVideo.valueR?.path)); return _getQualityChip( title: element.resolution ?? '', @@ -1077,7 +1084,7 @@ class NamidaVideoControlsState extends State with TickerPro } }, ); - }).toList(), + }), ], child: Padding( padding: const EdgeInsets.all(4.0), @@ -1093,19 +1100,15 @@ class NamidaVideoControlsState extends State with TickerPro ), child: Obx( () { - final isAudio = widget.isLocal ? VideoController.inst.currentVideo.value == null : Player.inst.isAudioOnlyPlayback; + final isAudio = widget.isLocal ? VideoController.inst.currentVideo.valueR == null : settings.ytIsAudioOnlyMode.valueR; String? qt; if (!isAudio) { if (widget.isLocal) { - final video = VideoController.inst.currentVideo.value; + final video = VideoController.inst.currentVideo.valueR; qt = video == null ? null : '${video.resolution}p${video.framerateText()}'; } else { - qt = Player.inst.currentVideoStream?.resolution; - if (qt == null) { - final c = Player.inst.currentCachedVideo; - qt = c == null ? null : '${c.resolution}p${c.framerateText()}'; - } + qt = Player.inst.currentVideoStream.valueR?.resolution; } } @@ -1200,26 +1203,26 @@ class NamidaVideoControlsState extends State with TickerPro children: [ Obx( () => Text( - "${Player.inst.nowPlayingPosition.milliSecondsLabel}/", + "${Player.inst.nowPlayingPositionR.milliSecondsLabel}/", style: context.textTheme.displayMedium?.copyWith( - fontSize: 13.5.multipliedFontScale, + fontSize: 13.5, color: itemsColor, ), ), ), Obx( () { - int totalDurMs = Player.inst.getCurrentVideoDuration.inMilliseconds; + int totalDurMs = Player.inst.getCurrentVideoDurationR.inMilliseconds; String prefix = ''; - if (settings.player.displayRemainingDurInsteadOfTotal.value) { - totalDurMs = totalDurMs - Player.inst.nowPlayingPosition; + if (settings.player.displayRemainingDurInsteadOfTotal.valueR) { + totalDurMs = totalDurMs - Player.inst.nowPlayingPositionR; prefix = '-'; } return Text( "$prefix${totalDurMs.milliSecondsLabel}", style: context.textTheme.displayMedium?.copyWith( - fontSize: 13.5.multipliedFontScale, + fontSize: 13.5, color: itemsColor, ), ); @@ -1236,7 +1239,7 @@ class NamidaVideoControlsState extends State with TickerPro // -- queue order Obx( () { - final queueL = (widget.isLocal ? Player.inst.currentQueue : Player.inst.currentQueueYoutube).length; + final queueL = Player.inst.currentQueue.valueR.length; if (queueL <= 1) return const SizedBox(); return BorderRadiusClip( borderRadius: borr8, @@ -1250,7 +1253,7 @@ class NamidaVideoControlsState extends State with TickerPro ), child: Obx( () => Text( - "${Player.inst.currentIndex + 1}/$queueL", + "${Player.inst.currentIndex.valueR + 1}/$queueL", style: context.textTheme.displaySmall?.copyWith(fontWeight: FontWeight.w600, color: itemsColor), ), ), @@ -1314,7 +1317,7 @@ class NamidaVideoControlsState extends State with TickerPro iconColor: itemsColor, onPressed: () { _startTimer(); - YTUtils().copyVideoUrl(Player.inst.getCurrentVideoId); + YTUtils().copyCurrentVideoUrl(Player.inst.getCurrentVideoId); }, ), SizedBox(width: widget.isFullScreen ? 12.0 : 10.0), @@ -1352,9 +1355,10 @@ class NamidaVideoControlsState extends State with TickerPro mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ const SizedBox(), - Obx( - () { - final shouldShowPrev = Player.inst.currentIndex != 0; + ObxO( + rx: Player.inst.currentIndex, + builder: (currentIndex) { + final shouldShowPrev = currentIndex != 0; return IgnorePointer( ignoring: !shouldShowPrev, child: Opacity( @@ -1394,8 +1398,8 @@ class NamidaVideoControlsState extends State with TickerPro color: Colors.black.withOpacity(0.3), child: Obx( () { - final currentPosition = Player.inst.nowPlayingPosition; - final currentTotalDur = Player.inst.currentItemDuration?.inMilliseconds ?? 0; + final currentPosition = Player.inst.nowPlayingPositionR; + final currentTotalDur = Player.inst.currentItemDuration.valueR?.inMilliseconds ?? 0; final reachedLastPosition = currentPosition != 0 && (currentPosition - currentTotalDur).abs() < 100; // 100ms allowance return reachedLastPosition @@ -1428,10 +1432,11 @@ class NamidaVideoControlsState extends State with TickerPro }, child: Padding( padding: const EdgeInsets.all(14.0), - child: Obx( - () => AnimatedSwitcher( + child: ObxO( + rx: Player.inst.isPlaying, + builder: (isPlaying) => AnimatedSwitcher( duration: const Duration(milliseconds: 200), - child: Player.inst.isPlaying + child: isPlaying ? Icon( Broken.pause, size: 40.0, @@ -1453,38 +1458,44 @@ class NamidaVideoControlsState extends State with TickerPro ), ), ), - Obx( - () { - final shouldShowNext = Player.inst.currentIndex != Player.inst.currentQueueYoutube.length - 1; - return IgnorePointer( - ignoring: !shouldShowNext, - child: Opacity( - opacity: shouldShowNext ? 1.0 : 0.0, - child: ClipOval( - child: NamidaBgBlur( - blur: 2, - child: ColoredBox( - color: Colors.black.withOpacity(0.2), - child: NamidaIconButton( - icon: null, - horizontalPadding: 0.0, - padding: EdgeInsets.zero, - onPressed: () { - Player.inst.next(); - _startTimer(); - }, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Icon( - Broken.next, - size: 30.0, - color: itemsColor, - ), - )), + ObxO( + rx: Player.inst.currentIndex, + builder: (currentIndex) { + return ObxO( + rx: Player.inst.currentQueue, + builder: (ytqueue) { + final shouldShowNext = currentIndex != ytqueue.length - 1; + return IgnorePointer( + ignoring: !shouldShowNext, + child: Opacity( + opacity: shouldShowNext ? 1.0 : 0.0, + child: ClipOval( + child: NamidaBgBlur( + blur: 2, + child: ColoredBox( + color: Colors.black.withOpacity(0.2), + child: NamidaIconButton( + icon: null, + horizontalPadding: 0.0, + padding: EdgeInsets.zero, + onPressed: () { + Player.inst.next(); + _startTimer(); + }, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Icon( + Broken.next, + size: 30.0, + color: itemsColor, + ), + )), + ), + ), ), ), - ), - ), + ); + }, ); }, ), @@ -1493,7 +1504,7 @@ class NamidaVideoControlsState extends State with TickerPro ), ), Obx( - () => Player.inst.shouldShowLoadingIndicator + () => Player.inst.shouldShowLoadingIndicatorR ? ThreeArchedCircle( color: itemsColor, size: 52.0, @@ -1544,7 +1555,7 @@ class NamidaVideoControlsState extends State with TickerPro left: context.width * 0.15, child: Obx( () { - final bri = _canShowBrightnessSlider.value ? _currentBrigthnessDim.value : null; + final bri = _canShowBrightnessSlider.valueR ? _currentBrigthnessDim.valueR : null; return _getVerticalSliderWidget( 'brightness', bri, @@ -1557,16 +1568,14 @@ class NamidaVideoControlsState extends State with TickerPro // ======= Volume Slider ======== Positioned( right: context.width * 0.15, - child: Obx( - () { - final vol = _currentDeviceVolume.value; - return _getVerticalSliderWidget( - 'volume', - vol, - Broken.volume_high, - view, - ); - }, + child: ObxO( + rx: _currentDeviceVolume, + builder: (vol) => _getVerticalSliderWidget( + 'volume', + vol, + Broken.volume_high, + view, + ), ), ), ], @@ -1604,7 +1613,7 @@ class __SpeedsEditorDialogState extends State<_SpeedsEditorDialog> { actions: [ TextButton( onPressed: NamidaNavigator.inst.closeDialog, - child: Text(lang.DONE), + child: NamidaButtonText(lang.DONE), ), NamidaButton( text: lang.ADD, diff --git a/lib/ui/widgets/waveform.dart b/lib/ui/widgets/waveform.dart index b43539bc..163611a6 100644 --- a/lib/ui/widgets/waveform.dart +++ b/lib/ui/widgets/waveform.dart @@ -1,5 +1,5 @@ +// ignore_for_file: avoid_rx_value_getter_outside_obx import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/class/track.dart'; import 'package:namida/controller/current_color.dart'; @@ -8,6 +8,7 @@ import 'package:namida/controller/namida_channel.dart'; import 'package:namida/controller/player_controller.dart'; import 'package:namida/controller/waveform_controller.dart'; import 'package:namida/core/extensions.dart'; +import 'package:namida/core/utils.dart'; class WaveformComponent extends StatefulWidget { final int durationInMilliseconds; @@ -16,12 +17,12 @@ class WaveformComponent extends StatefulWidget { final double barsMaxHeight; const WaveformComponent({ - Key? key, + super.key, this.durationInMilliseconds = 600, this.curve = Curves.easeInOutQuart, this.barsMinHeight = 3.0, this.barsMaxHeight = 64.0, - }) : super(key: key); + }); @override State createState() => WaveformComponentState(); @@ -47,9 +48,14 @@ class WaveformComponentState extends State with SingleTickerP reverseDuration: Duration(milliseconds: widget.durationInMilliseconds), ); - int get _currentDurationInMS { - final totalDur = Player.inst.currentItemDuration ?? (Player.inst.currentQueue.isNotEmpty ? Player.inst.nowPlayingTrack.duration.seconds : Duration.zero); - return totalDur.inMilliseconds; + int get _currentDurationInMSR { + final totalDur = Player.inst.currentItemDuration.valueR; + if (totalDur != null) return totalDur.inMilliseconds; + final current = Player.inst.currentItem.valueR; + if (current is Selectable) { + return current.track.duration * 1000; + } + return 0; } @override @@ -73,72 +79,73 @@ class WaveformComponentState extends State with SingleTickerP final decorationBoxBehind = DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(5.0.multipliedRadius), - color: context.theme.colorScheme.onBackground.withAlpha(40), + color: context.theme.colorScheme.onSurface.withAlpha(40), ), ); final decorationBoxFront = DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(5.0.multipliedRadius), - color: context.theme.colorScheme.onBackground.withAlpha(110), + color: context.theme.colorScheme.onSurface.withAlpha(110), ), ); - return Obx(() { - final enabled = WaveformController.inst.isWaveformUIEnabled.value; - _updateAnimation(enabled); - final downscaled = WaveformController.inst.currentWaveformUI; - final barWidth = view.physicalSize.shortestSide / view.devicePixelRatio / downscaled.length * 0.45; - return Center( - child: AnimatedBuilder( - animation: _animation, - builder: (context, _) => Stack( - children: [ - NamidaWaveBars( - heightPercentage: _animation.value, - decorationBox: decorationBoxBehind, - waveList: downscaled, - barWidth: barWidth, - barMinHeight: widget.barsMinHeight, - barMaxHeight: widget.barsMaxHeight, - ), - Obx( - () { - final seekValue = MiniPlayerController.inst.seekValue.value; - final position = seekValue != 0.0 ? seekValue : Player.inst.nowPlayingPosition; - final durInMs = _currentDurationInMS; - final percentage = (position / durInMs).clamp(0.0, durInMs.toDouble()); - return ShaderMask( - blendMode: BlendMode.srcIn, - shaderCallback: (Rect bounds) { - return LinearGradient( - tileMode: TileMode.decal, - stops: [0.0, percentage, percentage + 0.005, 1.0], - colors: [ - Color.alphaBlend(CurrentColor.inst.miniplayerColor.withAlpha(220), context.theme.colorScheme.onBackground), - Color.alphaBlend(CurrentColor.inst.miniplayerColor.withAlpha(180), context.theme.colorScheme.onBackground), - Colors.transparent, - Colors.transparent, - ], - ).createShader(bounds); + return ObxO( + rx: WaveformController.inst.isWaveformUIEnabled, + builder: (enabled) { + _updateAnimation(enabled); + final downscaled = WaveformController.inst.currentWaveformUI; + final barWidth = view.physicalSize.shortestSide / view.devicePixelRatio / downscaled.length * 0.45; + return Center( + child: AnimatedBuilder( + animation: _animation, + builder: (context, _) => Stack( + children: [ + NamidaWaveBars( + heightPercentage: _animation.value, + decorationBox: decorationBoxBehind, + waveList: downscaled, + barWidth: barWidth, + barMinHeight: widget.barsMinHeight, + barMaxHeight: widget.barsMaxHeight, + ), + Obx( + () { + final seekValue = MiniPlayerController.inst.seekValue.valueR; + final position = seekValue != 0.0 ? seekValue : Player.inst.nowPlayingPositionR; + final durInMs = _currentDurationInMSR; + final percentage = (position / durInMs).clamp(0.0, durInMs.toDouble()); + return ShaderMask( + blendMode: BlendMode.srcIn, + shaderCallback: (Rect bounds) { + return LinearGradient( + tileMode: TileMode.decal, + stops: [0.0, percentage, percentage + 0.005, 1.0], + colors: [ + Color.alphaBlend(CurrentColor.inst.miniplayerColor.withAlpha(220), context.theme.colorScheme.onSurface), + Color.alphaBlend(CurrentColor.inst.miniplayerColor.withAlpha(180), context.theme.colorScheme.onSurface), + Colors.transparent, + Colors.transparent, + ], + ).createShader(bounds); + }, + child: SizedBox( + width: namida.width - 16.0 / 2, + child: NamidaWaveBars( + heightPercentage: _animation.value, + decorationBox: decorationBoxFront, + waveList: downscaled, + barWidth: barWidth, + barMinHeight: widget.barsMinHeight, + barMaxHeight: widget.barsMaxHeight, + ), + ), + ); }, - child: SizedBox( - width: Get.width - 16.0 / 2, - child: NamidaWaveBars( - heightPercentage: _animation.value, - decorationBox: decorationBoxFront, - waveList: downscaled, - barWidth: barWidth, - barMinHeight: widget.barsMinHeight, - barMaxHeight: widget.barsMaxHeight, - ), - ), - ); - }, + ), + ], ), - ], - ), - ), - ); - }); + ), + ); + }); } } diff --git a/lib/youtube/class/download_progress.dart b/lib/youtube/class/download_progress.dart new file mode 100644 index 00000000..87133043 --- /dev/null +++ b/lib/youtube/class/download_progress.dart @@ -0,0 +1,20 @@ +class DownloadProgress { + final int progress; + final int totalProgress; + + const DownloadProgress({ + required this.progress, + required this.totalProgress, + }); + + double get percentage => progress / totalProgress; + + String? percentageText({String? prefix}) { + final p = percentage; + if (p.isFinite) { + final String res = (percentage * 100).toStringAsFixed(0); + return prefix != null ? "$prefix $res%" : "$res%"; + } + return null; + } +} diff --git a/lib/youtube/class/yt_thumbnail_wrapper.dart b/lib/youtube/class/yt_thumbnail_wrapper.dart new file mode 100644 index 00000000..1f838173 --- /dev/null +++ b/lib/youtube/class/yt_thumbnail_wrapper.dart @@ -0,0 +1,13 @@ +import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; + +class YTThumbnail { + final String id; + const YTThumbnail(this.id); + String get maxResUrl => StreamThumbnail(id).maxresdefault; + String get hqdefault => StreamThumbnail(id).hqdefault; + String get mqdefault => StreamThumbnail(id).mqdefault; + String get sddefault => StreamThumbnail(id).sddefault; + String get lowres => StreamThumbnail(id).lowres; + List get allQualitiesByHighest => [maxResUrl, hqdefault, mqdefault, sddefault, lowres]; + List get allQualitiesExceptHighest => [hqdefault, mqdefault, sddefault, lowres]; +} diff --git a/lib/youtube/controller/parallel_downloads_controller.dart b/lib/youtube/controller/parallel_downloads_controller.dart index feca108f..b5c49eb1 100644 --- a/lib/youtube/controller/parallel_downloads_controller.dart +++ b/lib/youtube/controller/parallel_downloads_controller.dart @@ -1,8 +1,7 @@ import 'dart:async'; -import 'package:get/get_rx/get_rx.dart'; - import 'package:namida/core/extensions.dart'; +import 'package:namida/core/utils.dart'; class YoutubeParallelDownloadsHandler { static final YoutubeParallelDownloadsHandler inst = YoutubeParallelDownloadsHandler._internal(); @@ -17,7 +16,7 @@ class YoutubeParallelDownloadsHandler { void refreshCompleterStatus() => _tryReAssignMaxParallelDownloadsCompleter(); int _currentDownloadingItemsCount = 0; - final _maxParallelDownloadingItems = 1.obs; + final _maxParallelDownloadingItems = 1.obso; /// used to control the parallel process, stopping the download loop or continuing it. Completer? _maxParallelDownloadsCompleter; diff --git a/lib/youtube/controller/youtube_controller.dart b/lib/youtube/controller/youtube_controller.dart index 848833fc..91f85861 100644 --- a/lib/youtube/controller/youtube_controller.dart +++ b/lib/youtube/controller/youtube_controller.dart @@ -6,7 +6,6 @@ import 'dart:isolate'; import 'package:flutter/widgets.dart'; import 'package:flutter_html/flutter_html.dart'; -import 'package:get/get_rx/src/rx_types/rx_types.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart' hide EnumUtils; import 'package:namida/base/ports_provider.dart'; @@ -21,47 +20,17 @@ import 'package:namida/controller/thumbnail_manager.dart'; import 'package:namida/core/constants.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/namida_converter_ext.dart'; +import 'package:namida/core/utils.dart'; +import 'package:namida/youtube/class/download_progress.dart'; import 'package:namida/youtube/class/youtube_item_download_config.dart'; import 'package:namida/youtube/controller/parallel_downloads_controller.dart'; import 'package:namida/youtube/controller/youtube_ongoing_finished_downloads.dart'; import 'package:namida/youtube/yt_utils.dart'; -class YTThumbnail { - final String id; - const YTThumbnail(this.id); - String get maxResUrl => StreamThumbnail(id).maxresdefault; - String get hqdefault => StreamThumbnail(id).hqdefault; - String get mqdefault => StreamThumbnail(id).mqdefault; - String get sddefault => StreamThumbnail(id).sddefault; - String get lowres => StreamThumbnail(id).lowres; - List get allQualitiesByHighest => [maxResUrl, hqdefault, mqdefault, sddefault, lowres]; - List get allQualitiesExceptHighest => [hqdefault, mqdefault, sddefault, lowres]; -} - -class DownloadProgress { - final int progress; - final int totalProgress; - - const DownloadProgress({ - required this.progress, - required this.totalProgress, - }); - - double get percentage => progress / totalProgress; - - String? percentageText({String? prefix}) { - final p = percentage; - if (p.isFinite) { - final String res = (percentage * 100).toStringAsFixed(0); - return prefix != null ? "$prefix $res%" : "$res%"; - } - return null; - } -} - class YoutubeController { static YoutubeController get inst => _instance; static final YoutubeController _instance = YoutubeController._internal(); + YoutubeController._internal() { scrollController.addListener(() { final pixels = scrollController.positions.lastOrNull?.pixels; @@ -73,7 +42,7 @@ class YoutubeController { int get _defaultMiniplayerDimSeconds => settings.ytMiniplayerDimAfterSeconds.value; double get _defaultMiniplayerOpacity => settings.ytMiniplayerDimOpacity.value; - bool get canDimMiniplayer => _canDimMiniplayer.value; + RxBaseCore get canDimMiniplayer => _canDimMiniplayer; final _canDimMiniplayer = false.obs; Timer? _dimTimer; void cancelDimTimer() { @@ -92,7 +61,8 @@ class YoutubeController { } final scrollController = ScrollController(); - bool get shouldShowGlowUnderVideo => _shouldShowGlowUnderVideo.value; + + RxBaseCore get shouldShowGlowUnderVideo => _shouldShowGlowUnderVideo; final _shouldShowGlowUnderVideo = false.obs; final homepageFeed = [].obs; @@ -172,7 +142,7 @@ class YoutubeController { final completer2 = Completer(); Directory(paths.$1).listAllIsolate().then((value) { - value.loop((e, index) { + value.loop((e) { if (e is File) { try { final map = e.readAsJsonSync(); @@ -183,7 +153,7 @@ class YoutubeController { completer1.complete(); }); Directory(paths.$2).listAllIsolate().then((value) { - value.loop((e, index) { + value.loop((e) { if (e is File) { try { final map = e.readAsJsonSync(); @@ -359,7 +329,7 @@ class YoutubeController { final excess = map.length - 2000; if (excess > 0) { final excessKeys = map.keys.take(excess).toList(); - excessKeys.loop((k, _) => map.remove(k)); + excessKeys.loop((k) => map.remove(k)); } await _saveTemporarelyVideoInfoIsolate.thready({ @@ -373,7 +343,7 @@ class YoutubeController { final dirPath = p['dirPath'] as String; final entries = p['entries'] as List>>; - entries.loop((e, index) { + entries.loop((e) { final file = File('$dirPath${e.key}.txt'); file.writeAsJsonSync(e.value); }); @@ -381,7 +351,7 @@ class YoutubeController { /// Checks if the requested id is still playing, since most functions are async and will often /// take time to fetch from internet, and user may have played other vids, this covers such cases. - bool _canSafelyModifyMetadata(String id) => Player.inst.nowPlayingVideoID?.id == id; + bool _canSafelyModifyMetadata(String id) => Player.inst.currentVideo?.id == id; Future prepareHomeFeed() async { homepageFeed.clear(); @@ -418,7 +388,7 @@ class YoutubeController { currentRelatedVideos.value = List.filled(20, null, growable: true); final items = await NewPipeExtractorDart.videos.getRelatedStreams(id.toYTUrl()); _fillTempVideoInfoMap(items.whereType()); - items.loop((p, index) { + items.loop((p) { if (p is YoutubePlaylist) { YoutubeController.inst.getPlaylistStreams(p, forceInitial: true); } @@ -463,9 +433,7 @@ class YoutubeController { Future _fetchComments(String id, {bool forceRequest = false}) async { currentTotalCommentsCount.value = null; - currentComments - ..clear() - ..addAll(List.filled(20, null)); + currentComments.assignAll(List.filled(20, null)); // -- Fetching Comments. final fetchedComments = []; @@ -521,7 +489,7 @@ class YoutubeController { // -- saving to cache final cachedFile = File("${AppDirs.YT_METADATA_COMMENTS}$id.txt"); - _saveCommentsToStorage(cachedFile, currentComments); + _saveCommentsToStorage(cachedFile, currentComments.value); } } @@ -545,7 +513,7 @@ class YoutubeController { } VideoStream getPreferredStreamQuality(List streams, {List qualities = const [], bool preferIncludeWebm = false}) { - final preferredQualities = (qualities.isNotEmpty ? qualities : settings.youtubeVideoQualities).map((element) => element.settingLabeltoVideoLabel()); + final preferredQualities = (qualities.isNotEmpty ? qualities : settings.youtubeVideoQualities.value).map((element) => element.settingLabeltoVideoLabel()); VideoStream? plsLoop(bool webm) { for (int i = 0; i < streams.length; i++) { final q = streams[i]; @@ -727,22 +695,22 @@ class YoutubeController { } void _loopMapAndPostNotification({ - required Map> bigMap, + required Map> bigMap, required int Function(String key, int progress) speedInBytes, required DateTime startTime, required bool isAudio, }) { final downloadingText = isAudio ? "Audio" : "Video"; for (final bigEntry in bigMap.entries.toList()) { - final map = bigEntry.value; + final map = bigEntry.value.value; final videoId = bigEntry.key; for (final entry in map.entries.toList()) { final p = entry.value.progress; final tp = entry.value.totalProgress; final title = getVideoName(videoId) ?? videoId; final speedB = speedInBytes(videoId, entry.value.progress); - currentSpeedsInByte[videoId] ??= {}.obs; - currentSpeedsInByte[videoId]![entry.key] = speedB; + currentSpeedsInByte.value[videoId] ??= {}.obs; + currentSpeedsInByte.value[videoId]![entry.key] = speedB; if (p / tp >= 1) { map.remove(entry.key); } else { @@ -813,7 +781,7 @@ class YoutubeController { _loopMapAndPostNotification( startTime: startTime, isAudio: false, - bigMap: downloadsVideoProgressMap, + bigMap: downloadsVideoProgressMap.value, speedInBytes: (key, newProgress) { final previousProgress = _speedMapVideo[key] ?? 0; final speed = newProgress - previousProgress; @@ -824,7 +792,7 @@ class YoutubeController { _loopMapAndPostNotification( startTime: startTime, isAudio: true, - bigMap: downloadsAudioProgressMap, + bigMap: downloadsAudioProgressMap.value, speedInBytes: (key, newProgress) { final previousProgress = _speedMapAudio[key] ?? 0; final speed = newProgress - previousProgress; @@ -863,15 +831,15 @@ class YoutubeController { final aFile = File("$saveDirPath/.tempa_${ytitem.filename}"); final vFile = File("$saveDirPath/.tempv_${ytitem.filename}"); if (aFile.existsSync()) { - downloadsAudioProgressMap[ytitem.id] ??= {}.obs; - downloadsAudioProgressMap[ytitem.id]![ytitem.filename] = DownloadProgress( + downloadsAudioProgressMap.value[ytitem.id] ??= {}.obs; + downloadsAudioProgressMap.value[ytitem.id]![ytitem.filename] = DownloadProgress( progress: aFile.fileSizeSync() ?? 0, totalProgress: 0, ); } if (vFile.existsSync()) { - downloadsVideoProgressMap[ytitem.id] ??= {}.obs; - downloadsVideoProgressMap[ytitem.id]![ytitem.filename] = DownloadProgress( + downloadsVideoProgressMap.value[ytitem.id] ??= {}.obs; + downloadsVideoProgressMap.value[ytitem.id]![ytitem.filename] = DownloadProgress( progress: vFile.fileSizeSync() ?? 0, totalProgress: 0, ); @@ -907,7 +875,7 @@ class YoutubeController { for (final e in youtubeDownloadTasksMap.entries) { for (final config in e.value.values) { final groupName = e.key; - videosIds.loop((e, index) { + videosIds.loop((e) { if (e == config.id) { onMatch(groupName, config); } @@ -988,7 +956,7 @@ class YoutubeController { onMatch: onMatch, ); } else { - itemsConfig.loop((c, _) => onMatch(groupName, c)); + itemsConfig.loop((c) => onMatch(groupName, c)); } youtubeDownloadTasksInQueueMap.refresh(); } @@ -1027,7 +995,7 @@ class YoutubeController { if (remove) { final directory = Directory("${AppDirs.YOUTUBE_DOWNLOADS}$groupName"); final itemsToCancel = allInGroupName ? youtubeDownloadTasksMap[groupName]!.values.toList() : itemsConfig; - await itemsToCancel.loopFuture((c, _) async { + for (final c in itemsToCancel) { _downloadManager.stopDownload(file: _downloadClientsMap[groupName]?[c.filename]); _downloadClientsMap[groupName]?.remove(c.filename); _breakRetrievingInfoRequest(c); @@ -1036,16 +1004,18 @@ class YoutubeController { youtubeDownloadTasksInQueueMap[groupName]?.remove(c.filename); YTOnGoingFinishedDownloads.inst.youtubeDownloadTasksTempList.remove((groupName, c)); } - await File("$directory/${c.filename}").deleteIfExists(); + try { + await File("$directory/${c.filename}").delete(); + } catch (_) {} downloadedFilesMap[groupName]?[c.filename] = null; - }); + } // -- remove groups if emptied. if (youtubeDownloadTasksMap[groupName]?.isEmpty == true) { youtubeDownloadTasksMap.remove(groupName); } } else { - itemsConfig.loop((c, _) { + itemsConfig.loop((c) { youtubeDownloadTasksMap[groupName]![c.filename] = c; youtubeDownloadTasksInQueueMap[groupName]![c.filename] = true; }); @@ -1099,8 +1069,8 @@ class YoutubeController { final completerA = _completersVAI[config]!.$2; final completerI = _completersVAI[config]!.$3; - isFetchingData[videoID] ??= {}.obs; - isFetchingData[videoID]![config.filename] = true; + isFetchingData.value[videoID] ??= {}.obs; + isFetchingData.value[videoID]![config.filename] = true; try { final dummyVideoUrl = (config.videoStream?.url == null || config.videoStream?.url == ''); @@ -1321,8 +1291,8 @@ class YoutubeController { ); } - isDownloading[id] ??= {}.obs; - isDownloading[id]![filenameClean] = true; + isDownloading.value[id] ??= {}.obs; + isDownloading.value[id]![filenameClean] = true; _startNotificationTimer(); @@ -1376,7 +1346,7 @@ class YoutubeController { int bytesLength = 0; - downloadsVideoProgressMap[id] ??= {}.obs; + downloadsVideoProgressMap.value[id] ??= {}.obs; final downloadedFile = await _checkFileAndDownload( groupName: groupName, url: videoStream.url ?? '', @@ -1431,7 +1401,7 @@ class YoutubeController { } int bytesLength = 0; - downloadsAudioProgressMap[id] ??= {}.obs; + downloadsAudioProgressMap.value[id] ??= {}.obs; final downloadedFile = await _checkFileAndDownload( groupName: groupName, url: audioStream.url ?? '', @@ -1745,7 +1715,7 @@ class _YTDownloadManager with PortsProvider { Future stopDownloads({required List files}) async { if (files.isEmpty) return; - files.loop((e, _) => _onFileFinish(e.path, false)); + files.loop((e) => _onFileFinish(e.path, false)); final p = {'files': files, 'stop': true}; await sendPort(p); } @@ -1850,7 +1820,7 @@ class _YTDownloadManager with PortsProvider { } void _onFileFinish(String path, bool? value) { - if (value != null) _downloadCompleters[path].completeIfWasnt(value); + if (value != null) _downloadCompleters[path]?.completeIfWasnt(value); _downloadCompleters[path] = null; _progressPorts[path]?.close(); _progressPorts[path] = null; diff --git a/lib/youtube/controller/youtube_history_controller.dart b/lib/youtube/controller/youtube_history_controller.dart index 9588bdde..ff820a45 100644 --- a/lib/youtube/controller/youtube_history_controller.dart +++ b/lib/youtube/controller/youtube_history_controller.dart @@ -1,5 +1,4 @@ // ignore_for_file: non_constant_identifier_names - import 'dart:collection'; import 'dart:io'; @@ -16,6 +15,15 @@ class YoutubeHistoryController with HistoryManager { static final YoutubeHistoryController _instance = YoutubeHistoryController._internal(); YoutubeHistoryController._internal(); + @override + double daysToSectionExtent(List days) { + const trackTileExtent = Dimensions.youtubeCardItemExtent; + const dayHeaderExtent = kHistoryDayHeaderHeightWithPadding; + double total = 0; + days.loop((day) => total += dayToSectionExtent(day, trackTileExtent, dayHeaderExtent)); + return total; + } + Future replaceAllVideosInsideHistory(YoutubeID oldVideo, YoutubeID newVideo) async { await replaceTheseTracksInHistory( (e) => e.id == oldVideo.id, @@ -27,9 +35,6 @@ class YoutubeHistoryController with HistoryManager { ); } - @override - double get DAY_HEADER_HEIGHT_WITH_PADDING => kHistoryDayHeaderHeightWithPadding; - @override String get HISTORY_DIRECTORY => AppDirs.YT_HISTORY_PLAYLIST; @@ -40,13 +45,14 @@ class YoutubeHistoryController with HistoryManager { String mainItemToSubItem(YoutubeID item) => item.id; @override - Future<({SplayTreeMap> historyMap, Map> topItems})> prepareAllHistoryFilesFunction(String directoryPath) async { + Future> prepareAllHistoryFilesFunction(String directoryPath) async { return await _readHistoryFilesCompute.thready(directoryPath); } - static Future<({SplayTreeMap> historyMap, Map> topItems})> _readHistoryFilesCompute(String path) async { + static Future> _readHistoryFilesCompute(String path) async { final map = SplayTreeMap>((date1, date2) => date2.compareTo(date1)); final tempMapTopItems = >{}; + int totalCount = 0; for (final f in Directory(path).listSyncSafe()) { if (f is File) { try { @@ -54,8 +60,9 @@ class YoutubeHistoryController with HistoryManager { final dayOfVideo = int.parse(f.path.getFilenameWOExt); final listVideos = (response as List?)?.mapped((e) => YoutubeID.fromJson(e)) ?? []; map[dayOfVideo] = listVideos; + totalCount += listVideos.length; - listVideos.loop((e, index) { + listVideos.loop((e) { tempMapTopItems.addForce(e.id, e.dateTimeAdded.millisecondsSinceEpoch); }); } catch (e) { @@ -80,8 +87,11 @@ class YoutubeHistoryController with HistoryManager { return compare; }); final topItems = Map.fromEntries(sortedEntries); - - return (historyMap: map, topItems: topItems); + return HistoryPrepareInfo( + historyMap: map, + topItems: topItems, + totalItemsCount: totalCount, + ); } @override @@ -92,7 +102,4 @@ class YoutubeHistoryController with HistoryManager { @override bool get mostPlayedCustomIsStartOfDay => settings.ytMostPlayedCustomisStartOfDay.value; - - @override - double get trackTileItemExtent => Dimensions.youtubeCardItemExtent; } diff --git a/lib/youtube/controller/youtube_import_controller.dart b/lib/youtube/controller/youtube_import_controller.dart index 65920437..48536786 100644 --- a/lib/youtube/controller/youtube_import_controller.dart +++ b/lib/youtube/controller/youtube_import_controller.dart @@ -1,10 +1,11 @@ import 'dart:async'; import 'dart:io'; -import 'package:get/get.dart'; + import 'package:playlist_manager/module/playlist_id.dart'; import 'package:namida/class/video.dart'; import 'package:namida/core/extensions.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/youtube/class/youtube_id.dart'; import 'package:namida/youtube/class/youtube_subscription.dart'; import 'package:namida/youtube/controller/youtube_playlist_controller.dart'; @@ -42,7 +43,7 @@ class YoutubeImportController { } final completer = Completer(); - res.loop((playlist, index) { + res.loopAdv((playlist, index) { final details = playlist.$1.details; final plID = details != null ? PlaylistID(id: details.playlistID) : null; YoutubePlaylistController.inst.addNewPlaylistRaw( @@ -77,7 +78,7 @@ class YoutubeImportController { Future importSubscriptions(String subscriptionsFilePath) async { isImportingSubscriptions.value = true; final res = await _parseSubscriptions.thready(subscriptionsFilePath); - res.loop((e, index) { + res.loop((e) { final valInMap = YoutubeSubscriptionsController.inst.getChannel(e.id); YoutubeSubscriptionsController.inst.setChannel( e.id, @@ -108,7 +109,7 @@ class YoutubeImportController { List<_VideoEntry> getVideos(List lines) { final videos = <_VideoEntry>[]; - lines.loop((e, _) { + lines.loop((e) { try { final parts = e.split(','); // id, dateAdded if (parts.length >= 2) videos.add((id: parts[0], dateAdded: DateTime.tryParse(parts[1]))); // should be only 2, but maybe more stuff will be appended in future @@ -119,7 +120,7 @@ class YoutubeImportController { _YTPlaylistDetails getPlaylistDetailsOld(List header, List split) { final map = {}; - header.loop((part, index) => map[part.toLowerCase()] ??= split[index]); + header.loopAdv((part, index) => map[part.toLowerCase()] ??= split[index]); return ( playlistID: map['playlist id'] ?? '', @@ -134,7 +135,7 @@ class YoutubeImportController { _YTPlaylistDetails getPlaylistDetailsNew(List header, List split) { final map = {}; - header.loop((part, index) => map[part.toLowerCase().split('playlist').last.split('(').first] ??= split[index]); + header.loopAdv((part, index) => map[part.toLowerCase().split('playlist').last.split('(').first] ??= split[index]); return ( playlistID: map['playlist id'] ?? '', channelID: map['channel id'] ?? '', @@ -158,7 +159,7 @@ class YoutubeImportController { final plLines = plMetaFile.readAsLinesSync(); final header = plLines.removeAt(0); final headerParts = header.split(','); - plLines.loop((line, _) { + plLines.loop((line) { final splitted = line.split(','); final details = getPlaylistDetailsNew(headerParts, splitted); playlistsMetadata[details.name] = details; @@ -166,7 +167,7 @@ class YoutubeImportController { } catch (_) {} } - files.loop((e, _) { + files.loop((e) { if (e is File) { try { String playlistName = e.path.getFilenameWOExt; @@ -203,7 +204,7 @@ class YoutubeImportController { final header = lines.removeAt(0); final list = <({String id, String title})>[]; if (header.split(',').length < 3) return list; - lines.loop((e, _) { + lines.loop((e) { try { final parts = e.split(','); // id, url, name if (parts.length >= 3) list.add((id: parts[0], title: parts[2])); // should be only 3, but maybe more stuff will be appended in future diff --git a/lib/youtube/controller/youtube_local_search_controller.dart b/lib/youtube/controller/youtube_local_search_controller.dart index 7270657f..37980b31 100644 --- a/lib/youtube/controller/youtube_local_search_controller.dart +++ b/lib/youtube/controller/youtube_local_search_controller.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'dart:isolate'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:jiffy/jiffy.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; @@ -11,6 +10,7 @@ import 'package:namida/base/ports_provider.dart'; import 'package:namida/class/video.dart'; import 'package:namida/core/constants.dart'; import 'package:namida/core/extensions.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; import 'package:namida/youtube/controller/youtube_history_controller.dart'; @@ -264,7 +264,7 @@ class YTLocalSearchController with PortsProvider { final completer2 = Completer(); Directory(dirStreamInfo).listAllIsolate().then((value) { - value.loop((file, _) { + value.loop((file) { try { final res = (file as File).readAsJsonSync(); if (res != null) { @@ -279,7 +279,7 @@ class YTLocalSearchController with PortsProvider { completer1.complete(); }); Directory(dirVideoInfo).listAllIsolate().then((value) { - value.loop((file, _) { + value.loop((file) { try { final res = (file as File).readAsJsonSync(); if (res != null) { @@ -348,7 +348,7 @@ class YTLocalSearchController with PortsProvider { void cleanResources({int afterSeconds = 10}) { _cancelDisposingTimer(); _disposingTimer = Timer(Duration(seconds: afterSeconds), () { - fillingCompleter.completeIfWasnt(); + fillingCompleter?.completeIfWasnt(); fillingCompleter = null; disposePort(); searchResults.clear(); diff --git a/lib/youtube/controller/youtube_ongoing_finished_downloads.dart b/lib/youtube/controller/youtube_ongoing_finished_downloads.dart index 71960697..5b4ef174 100644 --- a/lib/youtube/controller/youtube_ongoing_finished_downloads.dart +++ b/lib/youtube/controller/youtube_ongoing_finished_downloads.dart @@ -1,6 +1,5 @@ -import 'package:get/get.dart'; - import 'package:namida/core/extensions.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/youtube/class/youtube_item_download_config.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; @@ -18,10 +17,10 @@ class YTOnGoingFinishedDownloads { if (forIsGoing == null) return; void addToListy({required bool Function(bool fileExists, bool isDownloadingOrFetching) filter}) { - YoutubeController.inst.youtubeDownloadTasksMap.keys.toList().reverseLoop((key, index) { + YoutubeController.inst.youtubeDownloadTasksMap.keys.toList().reverseLoop((key) { final smallList = YoutubeController.inst.youtubeDownloadTasksMap[key]?.values.toList(); // -- reverseLoop to insert newer first. - smallList?.reverseLoop((v, index) { + smallList?.reverseLoop((v) { final fileExist = YoutubeController.inst.downloadedFilesMap[key]?[v.filename] != null; final isDownloadingOrFetching = (YoutubeController.inst.isDownloading[v.id]?[v.filename] ?? false) || (YoutubeController.inst.isFetchingData[v.id]?[v.filename] ?? false); if (filter(fileExist, isDownloadingOrFetching)) youtubeDownloadTasksTempList.add((key, v)); diff --git a/lib/youtube/controller/youtube_playlist_controller.dart b/lib/youtube/controller/youtube_playlist_controller.dart index 1e1b9831..7dfa4b31 100644 --- a/lib/youtube/controller/youtube_playlist_controller.dart +++ b/lib/youtube/controller/youtube_playlist_controller.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:get/get.dart'; import 'package:playlist_manager/module/playlist_id.dart'; import 'package:playlist_manager/playlist_manager.dart'; @@ -13,6 +12,7 @@ import 'package:namida/core/constants.dart'; import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/youtube/class/youtube_id.dart'; typedef YoutubePlaylist = GeneralPlaylist; @@ -23,6 +23,7 @@ class YoutubePlaylistController extends PlaylistManager { YoutubePlaylistController._internal(); final canReorderVideos = false.obs; + void resetCanReorder() => canReorderVideos.value = false; void addNewPlaylist( String name, { @@ -59,7 +60,7 @@ class YoutubePlaylistController extends PlaylistManager { if (preventDuplicates) { final existingIds = {}; - playlist.tracks.loop((e, index) { + playlist.tracks.loop((e) { existingIds[e.id] = true; }); // only add ids that doesnt exist inside playlist. @@ -120,9 +121,7 @@ class YoutubePlaylistController extends PlaylistManager { null; } - playlistsMap - ..clear() - ..addEntries(playlistList); + playlistsMap.assignAllEntries(playlistList); settings.save(ytPlaylistSort: sortBy, ytPlaylistSortReversed: reverse); } diff --git a/lib/youtube/controller/youtube_subscriptions_controller.dart b/lib/youtube/controller/youtube_subscriptions_controller.dart index c0710dac..0f55eabe 100644 --- a/lib/youtube/controller/youtube_subscriptions_controller.dart +++ b/lib/youtube/controller/youtube_subscriptions_controller.dart @@ -1,9 +1,8 @@ import 'dart:io'; -import 'package:get/get.dart'; - import 'package:namida/core/constants.dart'; import 'package:namida/core/extensions.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/youtube/class/youtube_subscription.dart'; class YoutubeSubscriptionsController { diff --git a/lib/youtube/controller/yt_generators_controller.dart b/lib/youtube/controller/yt_generators_controller.dart index 484eaef2..9f934763 100644 --- a/lib/youtube/controller/yt_generators_controller.dart +++ b/lib/youtube/controller/yt_generators_controller.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'dart:isolate'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; import 'package:history_manager/history_manager.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; import 'package:newpipeextractor_dart/utils/stringChecker.dart'; @@ -14,6 +13,7 @@ import 'package:namida/class/video.dart'; import 'package:namida/controller/navigator_controller.dart'; import 'package:namida/core/constants.dart'; import 'package:namida/core/extensions.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/youtube/class/youtube_id.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; import 'package:namida/youtube/controller/youtube_history_controller.dart'; @@ -98,7 +98,7 @@ class NamidaYTGenerator extends NamidaGeneratorBase with Port @override IsolateFunctionReturnBuild isolateFunction(SendPort port) { - final playlists = {for (final pl in YoutubePlaylistController.inst.playlistsMap.values) pl.name: pl.tracks}; + final playlists = {for (final pl in YoutubePlaylistController.inst.playlistsMap.value.values) pl.name: pl.tracks}; final params = { 'tempStreamInfo': YoutubeController.inst.tempVideoInfosFromStreams, 'dirStreamInfo': AppDirs.YT_METADATA_TEMP, @@ -159,7 +159,7 @@ class NamidaYTGenerator extends NamidaGeneratorBase with Port final results = []; final daysRange = p['daysRange'] as int; final videoToRemove = p['videoToRemove'] as String?; - allIds.loop((id, _) { + allIds.loop((id) { final dt = releaseDateMap[id]; if (dt != null && (dt.difference(dateReleased).inDays).abs() <= daysRange) { results.add(id); @@ -193,7 +193,7 @@ class NamidaYTGenerator extends NamidaGeneratorBase with Port final completer2 = Completer(); Directory(dirStreamInfo).listAllIsolate().then((value) { - value.loop((file, _) { + value.loop((file) { try { final res = (file as File).readAsJsonSync(); if (res != null) { @@ -209,7 +209,7 @@ class NamidaYTGenerator extends NamidaGeneratorBase with Port completer1.complete(); }); Directory(dirVideoInfo).listAllIsolate().then((value) { - value.loop((file, _) { + value.loop((file) { try { final res = (file as File).readAsJsonSync(); if (res != null) { @@ -241,7 +241,7 @@ class NamidaYTGenerator extends NamidaGeneratorBase with Port allIds.add(id); } } - favouritesPlaylist.loop((v, _) { + favouritesPlaylist.loop((v) { final id = v.id; if (allIdsAdded[id] == null) { allIds.add(id); @@ -249,7 +249,7 @@ class NamidaYTGenerator extends NamidaGeneratorBase with Port }); for (final pl in playlists.values) { - pl.loop((v, index) { + pl.loop((v) { final id = v.id; if (allIdsAdded[id] == null) { allIds.add(id); diff --git a/lib/youtube/functions/add_to_playlist_sheet.dart b/lib/youtube/functions/add_to_playlist_sheet.dart index bea1f5fb..d505a9dc 100644 --- a/lib/youtube/functions/add_to_playlist_sheet.dart +++ b/lib/youtube/functions/add_to_playlist_sheet.dart @@ -1,18 +1,18 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/controller/current_color.dart'; import 'package:namida/controller/navigator_controller.dart'; -import 'package:namida/youtube/controller/youtube_controller.dart'; -import 'package:namida/youtube/controller/youtube_playlist_controller.dart' as pc; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/main.dart'; import 'package:namida/ui/dialogs/edit_tags_dialog.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; +import 'package:namida/youtube/controller/youtube_controller.dart'; +import 'package:namida/youtube/controller/youtube_playlist_controller.dart' as pc; import 'package:namida/youtube/youtube_playlists_view.dart'; void showAddToPlaylistSheet({ @@ -35,10 +35,10 @@ void showAddToPlaylistSheet({ (ids.length > 3 ? '... + ${ids.length - 3}' : ''); await Future.delayed(Duration.zero); // delay bcz sometimes doesnt show - // ignore: use_build_context_synchronously await showModalBottomSheet( useRootNavigator: true, backgroundColor: Colors.transparent, + // ignore: use_build_context_synchronously context: context, builder: (context) { final bottomPadding = MediaQuery.viewInsetsOf(context).bottom + MediaQuery.paddingOf(context).bottom; @@ -68,8 +68,8 @@ void showAddToPlaylistSheet({ videoNamesSubtitle, style: context.textTheme.displaySmall, ) - : RichText( - text: TextSpan( + : Text.rich( + TextSpan( text: playlistNameToAdd, style: context.textTheme.displayMedium?.copyWith(fontWeight: FontWeight.w600), children: [ @@ -148,6 +148,7 @@ void showAddToPlaylistSheet({ Obx( () { const watchLater = 'Watch Later'; + pc.YoutubePlaylistController.inst.playlistsMap.valueR; final pl = pc.YoutubePlaylistController.inst.getPlaylist(watchLater); final idExist = pl?.tracks.firstWhereEff((e) => e.id == ids.firstOrNull) != null; return NamidaIconButton( diff --git a/lib/youtube/functions/download_sheet.dart b/lib/youtube/functions/download_sheet.dart index 8dc76f30..966a82a9 100644 --- a/lib/youtube/functions/download_sheet.dart +++ b/lib/youtube/functions/download_sheet.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; import 'package:namida/controller/current_color.dart'; @@ -13,6 +12,7 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/main.dart'; import 'package:namida/ui/dialogs/edit_tags_dialog.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; @@ -184,14 +184,14 @@ Future showDownloadVideoBottomSheet({ Text( title, style: context.textTheme.displayMedium?.copyWith( - fontSize: 12.0.multipliedFontScale, + fontSize: 12.0, ), ), if (subtitle != '') Text( subtitle, style: context.textTheme.displaySmall?.copyWith( - fontSize: 12.0.multipliedFontScale, + fontSize: 12.0, ), ), ], @@ -261,9 +261,9 @@ Future showDownloadVideoBottomSheet({ } await Future.delayed(Duration.zero); // delay bcz sometimes doesnt show - // ignore: use_build_context_synchronously await showModalBottomSheet( isScrollControlled: true, + // ignore: use_build_context_synchronously context: context, builder: (context) { final bottomPadding = MediaQuery.viewInsetsOf(context).bottom + MediaQuery.paddingOf(context).bottom; @@ -338,7 +338,7 @@ Future showDownloadVideoBottomSheet({ const SizedBox(width: 6.0), Obx( () { - final isWEBM = selectedAudioOnlyStream.value?.formatSuffix == 'webm'; + final isWEBM = selectedAudioOnlyStream.valueR?.formatSuffix == 'webm'; return Stack( alignment: Alignment.bottomRight, children: [ @@ -407,7 +407,7 @@ Future showDownloadVideoBottomSheet({ children: [ Obx( () { - final e = selectedAudioOnlyStream.value; + final e = selectedAudioOnlyStream.valueR; final subtitle = e == null ? null : "${e.bitrateText} • ${e.formatSuffix} • ${e.sizeInBytes?.fileSizeFormatted}"; return getTextWidget( title: lang.AUDIO, @@ -415,7 +415,7 @@ Future showDownloadVideoBottomSheet({ icon: Broken.audio_square, onCloseIconTap: () => selectedAudioOnlyStream.value = null, onSussyIconTap: () { - showAudioWebm.value = !showAudioWebm.value; + showAudioWebm.toggle(); if (showAudioWebm.value == false && selectedAudioOnlyStream.value?.formatSuffix == 'webm') { selectedAudioOnlyStream.value = video.value?.audioOnlyStreams?.firstOrNull; } @@ -424,7 +424,7 @@ Future showDownloadVideoBottomSheet({ }, ), Obx( - () => video.value?.audioOnlyStreams == null + () => video.valueR?.audioOnlyStreams == null ? Padding( padding: const EdgeInsets.all(8.0), child: ShimmerWrapper( @@ -436,15 +436,15 @@ Future showDownloadVideoBottomSheet({ ), ) : getPopupItem( - items: showAudioWebm.value - ? video.value!.audioOnlyStreams! - : video.value!.audioOnlyStreams!.where((element) => element.formatSuffix != 'webm').toList(), + items: showAudioWebm.valueR + ? video.valueR!.audioOnlyStreams! + : video.valueR!.audioOnlyStreams!.where((element) => element.formatSuffix != 'webm').toList(), itemBuilder: (element) { return Obx( () { final cacheFile = element.getCachedFile(videoId); return getQualityButton( - selected: selectedAudioOnlyStream.value == element, + selected: selectedAudioOnlyStream.valueR == element, cacheExists: cacheFile != null, title: "${element.codec} • ${element.sizeInBytes?.fileSizeFormatted}", subtitle: "${element.formatSuffix} • ${element.bitrateText}", @@ -458,7 +458,7 @@ Future showDownloadVideoBottomSheet({ getDivider(), Obx( () { - final e = selectedVideoOnlyStream.value; + final e = selectedVideoOnlyStream.valueR; final subtitle = e == null ? null : "${e.resolution} • ${e.sizeInBytes?.fileSizeFormatted}"; return getTextWidget( title: lang.VIDEO, @@ -466,7 +466,7 @@ Future showDownloadVideoBottomSheet({ icon: Broken.video_square, onCloseIconTap: () => selectedVideoOnlyStream.value = null, onSussyIconTap: () { - showVideoWebm.value = !showVideoWebm.value; + showVideoWebm.toggle(); if (showVideoWebm.value == false && selectedVideoOnlyStream.value?.formatSuffix == 'webm') { selectedVideoOnlyStream.value = video.value?.videoOnlyStreams?.firstOrNull; } @@ -475,7 +475,7 @@ Future showDownloadVideoBottomSheet({ }, ), Obx( - () => video.value?.videoOnlyStreams == null + () => video.valueR?.videoOnlyStreams == null ? Padding( padding: const EdgeInsets.all(8.0), child: ShimmerWrapper( @@ -487,15 +487,15 @@ Future showDownloadVideoBottomSheet({ ), ) : getPopupItem( - items: showVideoWebm.value - ? video.value!.videoOnlyStreams! - : video.value!.videoOnlyStreams!.where((element) => element.formatSuffix != 'webm').toList(), + items: showVideoWebm.valueR + ? video.valueR!.videoOnlyStreams! + : video.valueR!.videoOnlyStreams!.where((element) => element.formatSuffix != 'webm').toList(), itemBuilder: (element) { return Obx( () { final cacheFile = element.getCachedFile(videoId); return getQualityButton( - selected: selectedVideoOnlyStream.value == element, + selected: selectedVideoOnlyStream.valueR == element, cacheExists: cacheFile != null, title: "${element.resolution} • ${element.sizeInBytes?.fileSizeFormatted}", subtitle: "${element.formatSuffix} • ${element.bitrateText}", @@ -511,12 +511,12 @@ Future showDownloadVideoBottomSheet({ ), const SizedBox(height: 12.0), Obx(() { - final videoOnly = selectedVideoOnlyStream.value != null && selectedAudioOnlyStream.value == null ? lang.VIDEO_ONLY : null; - final audioOnly = selectedVideoOnlyStream.value == null && selectedAudioOnlyStream.value != null ? lang.AUDIO_ONLY : null; - final audioAndVideo = selectedVideoOnlyStream.value != null && selectedAudioOnlyStream.value != null ? "${lang.VIDEO} + ${lang.AUDIO}" : null; + final videoOnly = selectedVideoOnlyStream.valueR != null && selectedAudioOnlyStream.valueR == null ? lang.VIDEO_ONLY : null; + final audioOnly = selectedVideoOnlyStream.valueR == null && selectedAudioOnlyStream.valueR != null ? lang.AUDIO_ONLY : null; + final audioAndVideo = selectedVideoOnlyStream.valueR != null && selectedAudioOnlyStream.valueR != null ? "${lang.VIDEO} + ${lang.AUDIO}" : null; - return RichText( - text: TextSpan( + return Text.rich( + TextSpan( text: "${lang.OUTPUT}: ", style: context.textTheme.displaySmall, children: [ @@ -560,7 +560,7 @@ Future showDownloadVideoBottomSheet({ Expanded( flex: 1, child: TextButton( - child: Text(lang.CANCEL), + child: NamidaButtonText(lang.CANCEL), onPressed: () => Navigator.pop(context), ), ), @@ -569,7 +569,7 @@ Future showDownloadVideoBottomSheet({ flex: 2, child: Obx( () { - final sizeSum = (selectedVideoOnlyStream.value?.sizeInBytes ?? 0) + (selectedAudioOnlyStream.value?.sizeInBytes ?? 0); + final sizeSum = (selectedVideoOnlyStream.valueR?.sizeInBytes ?? 0) + (selectedAudioOnlyStream.valueR?.sizeInBytes ?? 0); final enabled = sizeSum > 0; final sizeText = enabled ? "(${sizeSum.fileSizeFormatted})" : ''; return IgnorePointer( @@ -582,7 +582,7 @@ Future showDownloadVideoBottomSheet({ padding: const EdgeInsets.all(12.0), height: 48.0, bgColor: colorScheme, - decoration: filenameExists.value + decoration: filenameExists.valueR ? BoxDecoration( border: Border.all( width: 3.0, diff --git a/lib/youtube/functions/video_download_options.dart b/lib/youtube/functions/video_download_options.dart index 384907ba..d9daa855 100644 --- a/lib/youtube/functions/video_download_options.dart +++ b/lib/youtube/functions/video_download_options.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/controller/ffmpeg_controller.dart'; import 'package:namida/controller/navigator_controller.dart'; @@ -11,6 +10,7 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/dialogs/edit_tags_dialog.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; @@ -55,9 +55,9 @@ Future showVideoDownloadOptionsSheet({ } await Future.delayed(Duration.zero); // delay bcz sometimes doesnt show - // ignore: use_build_context_synchronously await showModalBottomSheet( isScrollControlled: true, + // ignore: use_build_context_synchronously context: context, builder: (context) { final bottomPadding = MediaQuery.viewInsetsOf(context).bottom + MediaQuery.paddingOf(context).bottom; @@ -92,7 +92,7 @@ Future showVideoDownloadOptionsSheet({ icon: Broken.document_code, visualDensity: const VisualDensity(horizontal: VisualDensity.minimumDensity, vertical: VisualDensity.minimumDensity), title: lang.SET_FILE_LAST_MODIFIED_AS_VIDEO_UPLOAD_DATE, - value: settings.downloadFilesWriteUploadDate.value, + value: settings.downloadFilesWriteUploadDate.valueR, onChanged: (isTrue) => settings.save(downloadFilesWriteUploadDate: !isTrue), ), ), @@ -101,7 +101,7 @@ Future showVideoDownloadOptionsSheet({ icon: Broken.tick_circle, visualDensity: const VisualDensity(horizontal: VisualDensity.minimumDensity, vertical: VisualDensity.minimumDensity), title: lang.KEEP_CACHED_VERSIONS, - value: settings.downloadFilesKeepCachedVersions.value, + value: settings.downloadFilesKeepCachedVersions.valueR, onChanged: (isTrue) => settings.save(downloadFilesKeepCachedVersions: !isTrue), ), ), @@ -193,7 +193,7 @@ Future showVideoDownloadOptionsSheet({ }, child: Text( lang.AUTO_EXTRACT_TAGS_FROM_FILENAME, - style: Get.textTheme.displaySmall?.copyWith( + style: namida.textTheme.displaySmall?.copyWith( decoration: TextDecoration.underline, decorationStyle: TextDecorationStyle.dashed, ), @@ -371,17 +371,18 @@ class YTDownloadOptionFolderListTileState extends State datesOfListen = const [], Color? colorScheme}) async { showListensDialog( - datesOfListen: datesOfListen.isNotEmpty ? datesOfListen : YoutubeHistoryController.inst.topTracksMapListens[videoId] ?? [], + datesOfListen: datesOfListen.isNotEmpty ? datesOfListen : YoutubeHistoryController.inst.topTracksMapListens.value[videoId] ?? [], colorScheme: colorScheme, colorSchemeFunction: () async { final image = ThumbnailManager.inst.getYoutubeThumbnailFromCacheSync(id: videoId); diff --git a/lib/youtube/functions/yt_playlist_utils.dart b/lib/youtube/functions/yt_playlist_utils.dart index c6dae58a..760c4cf2 100644 --- a/lib/youtube/functions/yt_playlist_utils.dart +++ b/lib/youtube/functions/yt_playlist_utils.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart' as yt; import 'package:playlist_manager/module/playlist_id.dart'; import 'package:share_plus/share_plus.dart'; @@ -11,6 +10,7 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/functions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/packages/three_arched_circle.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/class/youtube_id.dart'; @@ -95,14 +95,14 @@ extension YoutubePlaylistHostedUtils on yt.YoutubePlaylist { if (context != null) { await Future.delayed(Duration.zero); - // ignore: use_build_context_synchronously showModalBottomSheet( + // ignore: use_build_context_synchronously context: context, useRootNavigator: true, isDismissible: false, builder: (context) { final iconSize = context.width * 0.5; - final iconColor = context.theme.colorScheme.onBackground.withOpacity(0.6); + final iconColor = context.theme.colorScheme.onSurface.withOpacity(0.6); return SizedBox( width: context.width, child: Padding( @@ -114,7 +114,7 @@ extension YoutubePlaylistHostedUtils on yt.YoutubePlaylist { () => AnimatedSwitcher( key: const Key('circle_switch'), duration: switchAnimationDurHalf, - child: currentCount.value < totalCount.value || isTotalCountNull() + child: currentCount.valueR < totalCount.valueR || isTotalCountNull() ? ThreeArchedCircle( size: iconSize, color: iconColor, @@ -135,7 +135,7 @@ extension YoutubePlaylistHostedUtils on yt.YoutubePlaylist { const SizedBox(height: 8.0), Obx( () => Text( - '${currentCount.value.formatDecimal()}/${isTotalCountNull() ? '?' : totalCount.value.formatDecimal()}', + '${currentCount.valueR.formatDecimal()}/${isTotalCountNull() ? '?' : totalCount.valueR.formatDecimal()}', style: context.textTheme.displayLarge, ), ), @@ -155,9 +155,7 @@ extension YoutubePlaylistHostedUtils on yt.YoutubePlaylist { totalCount.value = playlist.streamCount < 0 ? playlist.streams.length : playlist.streamCount; } - void plsPop() { - if (context?.mounted ?? false) context.safePop(); - } + void plsPop() => context?.safePop(); void closeRxStreams() { onEnd?.call(); @@ -217,7 +215,7 @@ extension YoutubePlaylistHostedUtils on yt.YoutubePlaylist { final playlist = this; final infoLookup = {}; - playlist.streams.loop((e, index) { + playlist.streams.loop((e) { infoLookup[e.id ?? ''] = e; }); NamidaNavigator.inst.navigateTo( @@ -249,7 +247,7 @@ extension YoutubePlaylistHostedUtils on yt.YoutubePlaylist { final ids = []; final info = {}; - playlist.streams.loop((e, index) { + playlist.streams.loop((e) { final id = e.id; if (id != null) { ids.add(id); diff --git a/lib/youtube/pages/youtube_home_view.dart b/lib/youtube/pages/youtube_home_view.dart index 398cdff0..73634807 100644 --- a/lib/youtube/pages/youtube_home_view.dart +++ b/lib/youtube/pages/youtube_home_view.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:namida/controller/settings_controller.dart'; -import 'package:namida/core/dimensions.dart'; import 'package:namida/core/enums.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; @@ -23,11 +22,11 @@ class YouTubeHomeView extends StatelessWidget { onIndexChanged: (index) { settings.save(ytInitialHomePage: YTHomePages.values[index]); }, - children: [ - const YoutubePage(), - const YoutubeChannelsPage(), - YoutubePlaylistsView(bottomPadding: Dimensions.inst.globalBottomPaddingTotal, scrollable: false), - const YTDownloadsPage(), + children: const [ + YoutubePage(), + YoutubeChannelsPage(), + YoutubePlaylistsView(), + YTDownloadsPage(), ], ), ); diff --git a/lib/youtube/pages/youtube_page.dart b/lib/youtube/pages/youtube_page.dart index 6ef7db13..371b1899 100644 --- a/lib/youtube/pages/youtube_page.dart +++ b/lib/youtube/pages/youtube_page.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; -import 'package:namida/youtube/controller/youtube_controller.dart'; -import 'package:namida/core/extensions.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; +import 'package:namida/youtube/controller/youtube_controller.dart'; import 'package:namida/youtube/widgets/yt_video_card.dart'; +import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; class YoutubePage extends StatefulWidget { const YoutubePage({super.key}); @@ -34,7 +33,7 @@ class _YoutubePageState extends State with AutomaticKeepAliveClient return BackgroundWrapper( child: Obx( () { - final homepageFeed = YoutubeController.inst.homepageFeed; + final homepageFeed = YoutubeController.inst.homepageFeed.valueR; final feed = homepageFeed.isEmpty ? List.filled(10, null) : homepageFeed; if (feed.isNotEmpty && feed.first == null) { @@ -63,7 +62,7 @@ class _YoutubePageState extends State with AutomaticKeepAliveClient padding: const EdgeInsets.all(24.0), child: Text( lang.HOME, - style: context.textTheme.displayLarge?.copyWith(fontSize: 38.0.multipliedFontScale), + style: context.textTheme.displayLarge?.copyWith(fontSize: 38.0), ), ), itemBuilder: (context, i) { @@ -78,7 +77,7 @@ class _YoutubePageState extends State with AutomaticKeepAliveClient ); }, itemCount: feed.length, - itemExtents: List.filled(feed.length, thumbnailItemExtent), + itemExtent: thumbnailItemExtent, ); }, ), diff --git a/lib/youtube/pages/yt_channel_subpage.dart b/lib/youtube/pages/yt_channel_subpage.dart index e88b6423..5df0bade 100644 --- a/lib/youtube/pages/yt_channel_subpage.dart +++ b/lib/youtube/pages/yt_channel_subpage.dart @@ -1,11 +1,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:jiffy/jiffy.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; import 'package:photo_view/photo_view.dart'; +import 'package:namida/base/youtube_channel_controller.dart'; import 'package:namida/controller/connectivity.dart'; import 'package:namida/controller/edit_delete_controller.dart'; import 'package:namida/controller/navigator_controller.dart'; @@ -14,10 +14,10 @@ import 'package:namida/core/dimensions.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/class/youtube_id.dart'; import 'package:namida/youtube/class/youtube_subscription.dart'; -import 'package:namida/base/youtube_channel_controller.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; import 'package:namida/youtube/controller/youtube_subscriptions_controller.dart'; import 'package:namida/youtube/widgets/yt_subscribe_buttons.dart'; @@ -79,7 +79,7 @@ class _YTChannelSubpageState extends YoutubeChannelController String title = lang.COPIED_ARTWORK; String subtitle = '${lang.SAVED_IN} $saveDirPath'; // ignore: use_build_context_synchronously - Color snackColor = context.theme.colorScheme.background; + Color snackColor = context.theme.colorScheme.surface; if (saveDirPath == null) { title = lang.ERROR; @@ -188,7 +188,7 @@ class _YTChannelSubpageState extends YoutubeChannelController subsCount < 2 ? lang.SUBSCRIBER : lang.SUBSCRIBERS, ].join(' '), style: context.textTheme.displayMedium?.copyWith( - fontSize: 12.0.multipliedFontScale, + fontSize: 12.0, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -217,9 +217,9 @@ class _YTChannelSubpageState extends YoutubeChannelController borderRadius: 8.0, icon: Broken.task_square, text: lang.LOAD_ALL, - enabled: !isLoadingMoreUploads.value && !lastLoadingMoreWasEmpty.value, + enabled: !isLoadingMoreUploads.valueR && !lastLoadingMoreWasEmpty.valueR, disableWhenLoading: false, - showLoadingWhenDisabled: !lastLoadingMoreWasEmpty.value, + showLoadingWhenDisabled: !lastLoadingMoreWasEmpty.valueR, onTap: () async { _canKeepLoadingMore = !_canKeepLoadingMore; while (_canKeepLoadingMore && !lastLoadingMoreWasEmpty.value && ConnectivityController.inst.hasConnection) { @@ -290,7 +290,7 @@ class _YTChannelSubpageState extends YoutubeChannelController .toList(), infoLookupCallback: () { final m = {}; - streamsList.loop((e, index) { + streamsList.loop((e) { m[e.id ?? ''] = e; }); return m; @@ -327,7 +327,7 @@ class _YTChannelSubpageState extends YoutubeChannelController }, listview: (controller) { return ListView.builder( - padding: EdgeInsets.only(bottom: Dimensions.inst.globalBottomPaddingTotal), + padding: EdgeInsets.only(bottom: Dimensions.inst.globalBottomPaddingTotalR), controller: controller, itemExtent: thumbnailItemExtent, itemCount: streamsList.length, diff --git a/lib/youtube/pages/yt_channels_page.dart b/lib/youtube/pages/yt_channels_page.dart index d4ab6533..68daf252 100644 --- a/lib/youtube/pages/yt_channels_page.dart +++ b/lib/youtube/pages/yt_channels_page.dart @@ -1,6 +1,5 @@ import 'package:calendar_date_picker2/calendar_date_picker2.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:jiffy/jiffy.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; @@ -12,6 +11,7 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/functions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/animated_widgets.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/settings/extra_settings.dart'; @@ -150,7 +150,7 @@ class _YoutubeChannelsPageState extends YoutubeChannelController Dimensions.inst.globalBottomPaddingEffective - 6.0; + double get _listBottomPadding => Dimensions.inst.globalBottomPaddingEffectiveR - 6.0; final _listTopPadding = 6.0; double get listHeight => _thumbSize + 12 * 2 + _listBottomPadding + _listTopPadding; @@ -230,7 +230,7 @@ class _YoutubeChannelsPageState extends YoutubeChannelController NamidaInkWellButton( icon: Broken.add_circle, text: lang.IMPORT, - enabled: !YoutubeImportController.inst.isImportingSubscriptions.value, + enabled: !YoutubeImportController.inst.isImportingSubscriptions.valueR, onTap: _onSubscriptionFileImportTap, ), ), @@ -314,7 +314,7 @@ class _YoutubeChannelsPageState extends YoutubeChannelController isLoadingMoreUploads.value + () => isLoadingMoreUploads.valueR ? const Padding( padding: EdgeInsets.all(8.0), child: Stack( @@ -427,7 +427,7 @@ class _YoutubeChannelsPageState extends YoutubeChannelController CircularProgressIndicator( - value: _allChannelsStreamsLoading.value && _allChannelsStreamsProgress.value <= 0 ? null : _allChannelsStreamsProgress.value, + value: _allChannelsStreamsLoading.valueR && _allChannelsStreamsProgress.valueR <= 0 ? null : _allChannelsStreamsProgress.valueR, strokeWidth: 2.0, ), ), @@ -447,7 +447,7 @@ class _YoutubeChannelsPageState extends YoutubeChannelController NamidaWheelSlider( - totalCount: 10, - initValue: tempCount.value, - onValueChanged: (val) => tempCount.value = val.withMinimum(1), - text: tempCount.value.toString(), - ), + () { + final temp = tempCount.valueR; + return NamidaWheelSlider( + totalCount: 10, + initValue: temp, + onValueChanged: (val) => tempCount.value = val.withMinimum(1), + text: temp.toString(), + ); + }, ), ), const SizedBox(height: 12.0), @@ -148,7 +152,7 @@ class YTDownloadsPage extends StatelessWidget { ); } - bool? get _isOnGoingSelected => YTOnGoingFinishedDownloads.inst.isOnGoingSelected.value; + bool? get _isOnGoingSelectedR => YTOnGoingFinishedDownloads.inst.isOnGoingSelected.valueR; set _isOnGoingSelected(bool? val) => YTOnGoingFinishedDownloads.inst.isOnGoingSelected.value = val; void _updateTempList(bool? forIsGoing) => YTOnGoingFinishedDownloads.inst.updateTempList(forIsGoing); void _refreshTempList() => YTOnGoingFinishedDownloads.inst.refreshList(); @@ -217,7 +221,7 @@ class YTDownloadsPage extends StatelessWidget { ), ), Obx( - () => _isOnGoingSelected != null + () => _isOnGoingSelectedR != null ? Padding( padding: const EdgeInsets.symmetric(vertical: 12.0), child: Row( @@ -225,15 +229,15 @@ class YTDownloadsPage extends StatelessWidget { const SizedBox(width: 24.0), Text( _downloadTasksTempList.length.displayVideoKeyword, - style: context.textTheme.displayMedium?.copyWith(fontSize: 20.0.multipliedFontScale), + style: context.textTheme.displayMedium?.copyWith(fontSize: 20.0), ), - if (_isOnGoingSelected == true) ...[ + if (_isOnGoingSelectedR == true) ...[ const Spacer(), NamidaIconButton( icon: Broken.play, iconSize: 24.0, onPressed: () { - _downloadTasksTempList.loop((e, index) { + _downloadTasksTempList.loop((e) { YoutubeController.inst.resumeDownloadTasks(groupName: e.$1, itemsConfig: [e.$2]); }); }, @@ -242,7 +246,7 @@ class YTDownloadsPage extends StatelessWidget { icon: Broken.pause, iconSize: 24.0, onPressed: () { - _downloadTasksTempList.loop((e, index) { + _downloadTasksTempList.loop((e) { YoutubeController.inst.pauseDownloadTask( itemsConfig: [e.$2], groupName: e.$1, @@ -261,7 +265,7 @@ class YTDownloadsPage extends StatelessWidget { itemsLength: _downloadTasksTempList.length, ); if (confirmed) { - _downloadTasksTempList.loop((e, index) { + _downloadTasksTempList.loop((e) { YoutubeController.inst.cancelDownloadTask( itemsConfig: [e.$2], groupName: e.$1, @@ -286,7 +290,7 @@ class YTDownloadsPage extends StatelessWidget { return CustomScrollView( controller: sc, slivers: [ - _isOnGoingSelected == null + _isOnGoingSelectedR == null ? SliverList.builder( itemCount: keys.length, itemBuilder: (context, index) { @@ -371,17 +375,20 @@ class YTDownloadsPage extends StatelessWidget { }, ) : Obx( - () => SliverList.builder( - itemCount: _downloadTasksTempList.length, - itemBuilder: (context, index) { - final groupNameAndItem = _downloadTasksTempList[index]; - return YTDownloadTaskItemCard( - videos: _downloadTasksTempList.map((e) => e.$2).toList(), - index: index, - groupName: groupNameAndItem.$1, - ); - }, - ), + () { + final videos = _downloadTasksTempList.valueR.map((e) => e.$2).toList(); + return SliverList.builder( + itemCount: _downloadTasksTempList.length, + itemBuilder: (context, index) { + final groupNameAndItem = _downloadTasksTempList[index]; + return YTDownloadTaskItemCard( + videos: videos, + index: index, + groupName: groupNameAndItem.$1, + ); + }, + ); + }, ), kBottomPaddingWidgetSliver, ], diff --git a/lib/youtube/pages/yt_history_page.dart b/lib/youtube/pages/yt_history_page.dart index 0ba77b59..8cf5c489 100644 --- a/lib/youtube/pages/yt_history_page.dart +++ b/lib/youtube/pages/yt_history_page.dart @@ -1,15 +1,17 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:known_extents_list_view_builder/sliver_known_extents_list.dart'; +import 'package:history_manager/history_manager.dart'; import 'package:playlist_manager/module/playlist_id.dart'; import 'package:sticky_headers/sticky_headers.dart'; +import 'package:namida/base/history_days_rebuilder.dart'; import 'package:namida/controller/current_color.dart'; import 'package:namida/core/constants.dart'; import 'package:namida/core/dimensions.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; +import 'package:namida/youtube/class/youtube_id.dart'; import 'package:namida/youtube/controller/youtube_history_controller.dart'; import 'package:namida/youtube/widgets/yt_history_video_card.dart'; import 'package:namida/youtube/yt_utils.dart'; @@ -21,121 +23,85 @@ class YoutubeHistoryPage extends StatefulWidget { State createState() => _YoutubeHistoryPageState(); } -class _YoutubeHistoryPageState extends State { +class _YoutubeHistoryPageState extends State with HistoryDaysRebuilderMixin { @override - void initState() { - super.initState(); - YoutubeHistoryController.inst.canUpdateAllItemsExtentsInHistory = true; - YoutubeHistoryController.inst.calculateAllItemsExtentsInHistory(); - } - - @override - void dispose() { - YoutubeHistoryController.inst.canUpdateAllItemsExtentsInHistory = false; - super.dispose(); - } + HistoryManager get historyManager => YoutubeHistoryController.inst; @override Widget build(BuildContext context) { + const cardExtent = Dimensions.youtubeCardItemExtent; + const dayHeaderExtent = kYoutubeHistoryDayHeaderHeightWithPadding; + + const dayHeaderHeight = kYoutubeHistoryDayHeaderHeight; + final dayHeaderBgColor = Color.alphaBlend(context.theme.cardColor.withAlpha(100), context.theme.scaffoldBackgroundColor); + final dayHeaderSideColor = CurrentColor.inst.color; + final dayHeaderShadowColor = Color.alphaBlend(context.theme.shadowColor.withAlpha(160), context.theme.scaffoldBackgroundColor).withOpacity(0.4); + + final daysLength = historyDays.length; + return BackgroundWrapper( child: CustomScrollView( controller: YoutubeHistoryController.inst.scrollController, slivers: [ - Obx( - () { - final days = YoutubeHistoryController.inst.historyDays.toList(); - return SliverKnownExtentsList( - key: UniqueKey(), - itemExtents: YoutubeHistoryController.inst.allItemsExtentsHistory, - delegate: SliverChildBuilderDelegate( - childCount: YoutubeHistoryController.inst.historyDays.length, - (context, index) { - final day = days[index]; - final dayInMs = Duration(days: day).inMilliseconds; - final videos = YoutubeHistoryController.inst.historyMap.value[day] ?? []; + ObxO( + rx: YoutubeHistoryController.inst.historyMap, + builder: (history) => SliverVariedExtentList.builder( + key: ValueKey(daysLength), // rebuild after adding/removing day + itemExtentBuilder: (index, dimensions) { + final day = historyDays[index]; + return YoutubeHistoryController.inst.dayToSectionExtent(day, cardExtent, dayHeaderExtent); + }, + itemCount: daysLength, + itemBuilder: (context, index) { + final day = historyDays[index]; + final dayInMs = super.dayToMillis(day); + final videos = history[day] ?? []; - return StickyHeaderBuilder( - key: ValueKey(index), - builder: (context, stuckAmount) { - return Container( - clipBehavior: Clip.antiAlias, - width: context.width, - height: kYoutubeHistoryDayHeaderHeight, - decoration: BoxDecoration( - color: Color.alphaBlend(context.theme.cardColor.withAlpha(100), context.theme.scaffoldBackgroundColor), - border: Border( - left: BorderSide( - color: CurrentColor.inst.color, - width: (4.0).withMinimum(3.0), - ), - ), - boxShadow: [ - BoxShadow( - offset: const Offset(0, 8.0), - blurRadius: 12.0, - spreadRadius: 2.0, - color: Color.alphaBlend(context.theme.shadowColor.withAlpha(180), context.theme.scaffoldBackgroundColor).withOpacity(0.4), - ), - ], - ), - child: Row( - children: [ - const SizedBox(width: 12.0), - Expanded( - child: Text( - [ - dayInMs.dateFormattedOriginal, - videos.length.displayVideoKeyword, - ].join(' • '), - style: context.textTheme.displayMedium, - ), - ), - NamidaPopupWrapper( - openOnLongPress: false, - childrenDefault: () => YTUtils.getVideosMenuItems( - playlistName: k_PLAYLIST_NAME_HISTORY, - videos: videos, - ), - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 8.0), - child: Icon( - Broken.more, - size: 22.0, - ), - ), - ), - const SizedBox(width: 2.0), - ], - ), - ); - }, - content: Obx( - () => SizedBox( - height: YoutubeHistoryController.inst.allItemsExtentsHistory[index], - width: context.width, - child: ListView.builder( - padding: const EdgeInsets.only(bottom: kYoutubeHistoryDayListBottomPadding, top: kYoutubeHistoryDayListTopPadding), - primary: false, - itemExtent: Dimensions.youtubeCardItemExtent, - itemCount: videos.length, - itemBuilder: (context, i) { - return YTHistoryVideoCard( - videos: videos, - index: i, - day: day, - playlistID: const PlaylistID(id: k_PLAYLIST_NAME_HISTORY), - playlistName: k_PLAYLIST_NAME_HISTORY, - isImportantInCache: false, // long old history is lowkey useless - ); - }, - ), + return StickyHeader( + key: ValueKey(index), + header: NamidaHistoryDayHeaderBox( + height: dayHeaderHeight, + title: [ + dayInMs.dateFormattedOriginal, + videos.length.displayVideoKeyword, + ].join(' • '), + sideColor: dayHeaderSideColor, + bgColor: dayHeaderBgColor, + shadowColor: dayHeaderShadowColor, + menu: NamidaPopupWrapper( + openOnLongPress: false, + childrenDefault: () => YTUtils.getVideosMenuItems( + playlistName: k_PLAYLIST_NAME_HISTORY, + videos: videos, + ), + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 8.0), + child: Icon( + Broken.more, + size: 22.0, ), ), - ); - }, - ), - ); - }, + ), + ), + content: ListView.builder( + padding: const EdgeInsets.only(bottom: kYoutubeHistoryDayListBottomPadding, top: kYoutubeHistoryDayListTopPadding), + primary: false, + itemExtent: Dimensions.youtubeCardItemExtent, + itemCount: videos.length, + itemBuilder: (context, i) { + return YTHistoryVideoCard( + videos: videos, + index: i, + day: day, + playlistID: const PlaylistID(id: k_PLAYLIST_NAME_HISTORY), + playlistName: k_PLAYLIST_NAME_HISTORY, + isImportantInCache: false, // long old history is lowkey useless + ); + }, + ), + ); + }, + ), ), kBottomPaddingWidgetSliver, ], diff --git a/lib/youtube/pages/yt_local_search_results.dart b/lib/youtube/pages/yt_local_search_results.dart index 23a66208..f561f6dc 100644 --- a/lib/youtube/pages/yt_local_search_results.dart +++ b/lib/youtube/pages/yt_local_search_results.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; import 'package:namida/controller/navigator_controller.dart'; @@ -8,6 +7,7 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/packages/three_arched_circle.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/controller/youtube_local_search_controller.dart'; @@ -126,7 +126,7 @@ class YTLocalSearchResultsState extends State { () => Stack( alignment: Alignment.center, children: [ - if (YTLocalSearchController.inst.didLoadLookupLists.value == false) + if (YTLocalSearchController.inst.didLoadLookupLists.valueR == false) IgnorePointer( child: NamidaOpacity( opacity: 0.3, diff --git a/lib/youtube/pages/yt_playlist_download_subpage.dart b/lib/youtube/pages/yt_playlist_download_subpage.dart index cd54b780..1c36a0b1 100644 --- a/lib/youtube/pages/yt_playlist_download_subpage.dart +++ b/lib/youtube/pages/yt_playlist_download_subpage.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; import 'package:namida/controller/current_color.dart'; @@ -14,6 +13,8 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; +import 'package:namida/main.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/class/youtube_id.dart'; import 'package:namida/youtube/class/youtube_item_download_config.dart'; @@ -22,8 +23,6 @@ import 'package:namida/youtube/functions/download_sheet.dart'; import 'package:namida/youtube/functions/video_download_options.dart'; import 'package:namida/youtube/widgets/yt_thumbnail.dart'; -import 'package:namida/main.dart'; - class YTPlaylistDownloadPage extends StatefulWidget { final List ids; final String playlistName; @@ -53,7 +52,7 @@ class _YTPlaylistDownloadPageState extends State { bool downloadFilesWriteUploadDate = settings.downloadFilesWriteUploadDate.value; bool addAudioToLocalLibrary = true; bool overrideOldFiles = false; - final preferredQuality = (settings.youtubeVideoQualities.firstOrNull ?? kStockVideoQualities.first).obs; + final preferredQuality = (settings.youtubeVideoQualities.value.firstOrNull ?? kStockVideoQualities.first).obs; final downloadAudioOnly = false.obs; void _onItemTap(String id) => _selectedList.addOrRemove(id); @@ -77,7 +76,7 @@ class _YTPlaylistDownloadPageState extends State { } void _fillConfigMap() { - widget.ids.loop((e, index) { + widget.ids.loop((e) { final id = e.id; _configMap[id] = _getDummyDownloadConfig(id); }); @@ -100,7 +99,7 @@ class _YTPlaylistDownloadPageState extends State { } void _addAllYTIDsToSelected() { - _selectedList.addAll(widget.ids.map((e) => e.id)); + _selectedList.assignAll(widget.ids.map((e) => e.id)); } Future _onEditIconTap({ @@ -198,17 +197,20 @@ class _YTPlaylistDownloadPageState extends State { }, ), Obx( - () => CustomSwitchListTile( - visualDensity: visualDensity, - enabled: downloadAudioOnly.value, - icon: Broken.music_library_2, - title: lang.ADD_AUDIO_TO_LOCAL_LIBRARY, - value: downloadAudioOnly.value && addAudioToLocalLibrary, - onChanged: (isTrue) { - addAudioToLocalLibrary = !addAudioToLocalLibrary; - rebuildy(); - }, - ), + () { + final downloadAO = downloadAudioOnly.valueR; + return CustomSwitchListTile( + visualDensity: visualDensity, + enabled: downloadAO, + icon: Broken.music_library_2, + title: lang.ADD_AUDIO_TO_LOCAL_LIBRARY, + value: downloadAO && addAudioToLocalLibrary, + onChanged: (isTrue) { + addAudioToLocalLibrary = !addAudioToLocalLibrary; + rebuildy(); + }, + ); + }, ), CustomSwitchListTile( visualDensity: visualDensity, @@ -244,7 +246,7 @@ class _YTPlaylistDownloadPageState extends State { ), ) ], - child: Obx(() => Text(downloadAudioOnly.value ? lang.AUDIO_ONLY : preferredQuality.value)), + child: Obx(() => Text(downloadAudioOnly.valueR ? lang.AUDIO_ONLY : preferredQuality.valueR)), ), ), ], @@ -254,7 +256,7 @@ class _YTPlaylistDownloadPageState extends State { ); } - double get _bottomPaddingEffective => Dimensions.inst.globalBottomPaddingEffective; + double get _bottomPaddingEffective => Dimensions.inst.globalBottomPaddingEffectiveR; double _hmultiplier = 0.9; double _previousScale = 0.9; @@ -282,7 +284,7 @@ class _YTPlaylistDownloadPageState extends State { tooltip: lang.INVERT_SELECTION, icon: Broken.recovery_convert, onPressed: () { - widget.ids.loop((e, index) { + widget.ids.loop((e) { _selectedList.addOrRemove(e.id); }); }, @@ -301,7 +303,6 @@ class _YTPlaylistDownloadPageState extends State { : true, onChanged: (value) { if (_selectedList.length != widget.ids.length) { - _selectedList.clear(); _addAllYTIDsToSelected(); } else { _selectedList.clear(); @@ -319,7 +320,7 @@ class _YTPlaylistDownloadPageState extends State { visualDensity: VisualDensity.compact, trailingPadding: 12.0, playlistName: widget.playlistName, - initialFolder: _groupName.value, + initialFolder: _groupName.valueR, subtitle: (value) => "${AppDirs.YOUTUBE_DOWNLOADS}$value", onDownloadGroupNameChanged: (newGroupName) { _groupName.value = newGroupName; @@ -344,7 +345,7 @@ class _YTPlaylistDownloadPageState extends State { () { final isSelected = _selectedList.contains(id); final filename = _configMap[id]?.filename; - final fileExists = File("${AppDirs.YOUTUBE_DOWNLOADS}${_groupName.value}/$filename").existsSync(); + final fileExists = File("${AppDirs.YOUTUBE_DOWNLOADS}${_groupName.valueR}/$filename").existsSync(); return NamidaInkWell( animationDurationMS: 200, height: Dimensions.youtubeCardItemHeight * _hmultiplier, @@ -371,7 +372,7 @@ class _YTPlaylistDownloadPageState extends State { } if (latestIndex != null && index > latestIndex) { final selectedRange = widget.ids.getRange(latestIndex + 1, index + 1); - selectedRange.toList().loop((e, index) { + selectedRange.toList().loop((e) { if (!_selectedList.contains(e.id)) _selectedList.add(e.id); }); } else { @@ -402,7 +403,7 @@ class _YTPlaylistDownloadPageState extends State { const SizedBox(height: 6.0), Text( info?.name ?? id, - style: context.textTheme.displayMedium?.copyWith(fontSize: 15.0.multipliedFontScale * _hmultiplier), + style: context.textTheme.displayMedium?.copyWith(fontSize: 15.0 * _hmultiplier), maxLines: 2, overflow: TextOverflow.ellipsis, ), @@ -417,7 +418,7 @@ class _YTPlaylistDownloadPageState extends State { const SizedBox(width: 2.0), Text( info?.uploaderName ?? YoutubeController.inst.getVideoChannelName(id) ?? '', - style: context.textTheme.displaySmall?.copyWith(fontSize: 14.0.multipliedFontScale * _hmultiplier), + style: context.textTheme.displaySmall?.copyWith(fontSize: 14.0 * _hmultiplier), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -508,7 +509,7 @@ class _YTPlaylistDownloadPageState extends State { NamidaNavigator.inst.popPage(); YoutubeController.inst.downloadYoutubeVideos( groupName: widget.playlistName, - itemsConfig: _selectedList.map((id) => _configMap[id] ?? _getDummyDownloadConfig(id)).toList(), + itemsConfig: _selectedList.value.map((id) => _configMap[id] ?? _getDummyDownloadConfig(id)).toList(), useCachedVersionsIfAvailable: true, autoExtractTitleAndArtist: autoExtractTitleAndArtist, keepCachedVersionsIfDownloaded: keepCachedVersionsIfDownloaded, diff --git a/lib/youtube/pages/yt_playlist_subpage.dart b/lib/youtube/pages/yt_playlist_subpage.dart index 70719850..45c35110 100644 --- a/lib/youtube/pages/yt_playlist_subpage.dart +++ b/lib/youtube/pages/yt_playlist_subpage.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; -import 'package:get/get.dart'; -import 'package:known_extents_list_view_builder/known_extents_sliver_reorderable_list.dart'; import 'package:newpipeextractor_dart/models/stream_info_item.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart' as yt; import 'package:playlist_manager/module/playlist_id.dart'; +import 'package:namida/base/youtube_streams_manager.dart'; import 'package:namida/controller/navigator_controller.dart'; import 'package:namida/controller/player_controller.dart'; import 'package:namida/controller/settings_controller.dart'; @@ -17,6 +16,7 @@ import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/themes.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/pages/subpages/most_played_subpage.dart'; import 'package:namida/ui/pages/subpages/playlist_tracks_subpage.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; @@ -25,7 +25,6 @@ import 'package:namida/youtube/class/youtube_id.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; import 'package:namida/youtube/controller/youtube_history_controller.dart'; import 'package:namida/youtube/controller/youtube_playlist_controller.dart'; -import 'package:namida/base/youtube_streams_manager.dart'; import 'package:namida/youtube/functions/yt_playlist_utils.dart'; import 'package:namida/youtube/pages/yt_playlist_download_subpage.dart'; import 'package:namida/youtube/widgets/yt_history_video_card.dart'; @@ -45,7 +44,7 @@ class YTMostPlayedVideosPage extends StatelessWidget { ) .toList(); return MostPlayedItemsPage( - itemExtents: List.filled(videos.length, Dimensions.youtubeCardItemExtent), + itemExtent: Dimensions.youtubeCardItemExtent, historyController: YoutubeHistoryController.inst, customDateRange: settings.ytMostPlayedCustomDateRange, isTimeRangeChipEnabled: (type) => type == settings.ytMostPlayedTimeRange.value, @@ -133,6 +132,11 @@ class _YTNormalPlaylistSubpageState extends State { Widget build(BuildContext context) { const horizontalBigThumbPadding = 12.0; final bigThumbWidth = context.width - horizontalBigThumbPadding * 2; + Color? threeCColor; + late final threeC = ObxO( + rx: YoutubePlaylistController.inst.canReorderVideos, + builder: (canReorderVideos) => ThreeLineSmallContainers(enabled: canReorderVideos, color: threeCColor), + ); return AnimatedTheme( duration: const Duration(milliseconds: 300), data: AppThemes.inst.getAppTheme(bgColor, !context.isDarkMode), @@ -140,6 +144,7 @@ class _YTNormalPlaylistSubpageState extends State { child: NamidaScrollbarWithController( child: (sc) => Obx( () { + YoutubePlaylistController.inst.playlistsMap.valueR; final playlist = YoutubePlaylistController.inst.getPlaylist(playlistCurrentName); if (playlist == null) return const SizedBox(); final firstID = playlist.tracks.firstOrNull?.id; @@ -285,33 +290,38 @@ class _YTNormalPlaylistSubpageState extends State { ), ), const SliverPadding(padding: EdgeInsets.only(bottom: 24.0)), - SliverKnownExtentsReorderableList( - overlayOffset: Offset.zero, - onReorder: (oldIndex, newIndex) => YoutubePlaylistController.inst.reorderTrack(playlist, oldIndex, newIndex), - itemExtents: List.filled(playlist.tracks.length, Dimensions.youtubeCardItemExtent), - itemCount: playlist.tracks.length, - itemBuilder: (context, index) { - return YTHistoryVideoCard( - key: Key("$index"), - videos: playlist.tracks, - index: index, - reversedList: widget.reversedList, - day: null, - playlistID: playlist.playlistID, - playlistName: playlistCurrentName, - draggingEnabled: YoutubePlaylistController.inst.canReorderVideos.value, - draggableThumbnail: true, - showMoreIcon: true, - draggingBarsBuilder: (color) { - return Obx( - () => ThreeLineSmallContainers(enabled: YoutubePlaylistController.inst.canReorderVideos.value, color: color), - ); - }, - draggingThumbnailBuilder: (draggingTrigger) { - return Obx(() => YoutubePlaylistController.inst.canReorderVideos.value ? draggingTrigger : const SizedBox()); - }, - ); - }, + ObxO( + rx: YoutubePlaylistController.inst.canReorderVideos, + builder: (canReorderVideos) => NamidaSliverReorderableList( + onReorder: (oldIndex, newIndex) => YoutubePlaylistController.inst.reorderTrack(playlist, oldIndex, newIndex), + itemExtent: Dimensions.youtubeCardItemExtent, + itemCount: playlist.tracks.length, + itemBuilder: (context, index) { + return YTHistoryVideoCard( + key: ValueKey(index), + videos: playlist.tracks, + index: index, + reversedList: widget.reversedList, + day: null, + playlistID: playlist.playlistID, + playlistName: playlistCurrentName, + draggingEnabled: YoutubePlaylistController.inst.canReorderVideos.value, + openMenuOnLongPress: !canReorderVideos, + draggableThumbnail: true, + showMoreIcon: true, + draggingBarsBuilder: (color) { + threeCColor ??= color; + return threeC; + }, + draggingThumbnailBuilder: (draggingTrigger) { + return ObxO( + rx: YoutubePlaylistController.inst.canReorderVideos, + builder: (canReorderVideos) => canReorderVideos ? draggingTrigger : const SizedBox(), + ); + }, + ); + }, + ), ), kBottomPaddingWidgetSliver, ], @@ -575,7 +585,7 @@ class _YTHostedPlaylistSubpageState extends State with borderRadius: 8.0, icon: Broken.task_square, text: lang.LOAD_ALL, - enabled: !_isLoadingMoreItems.value && hasMoreStreamsLeft, + enabled: !_isLoadingMoreItems.valueR && hasMoreStreamsLeft, disableWhenLoading: false, showLoadingWhenDisabled: hasMoreStreamsLeft, onTap: () async { @@ -612,7 +622,7 @@ class _YTHostedPlaylistSubpageState extends State with ), SliverToBoxAdapter( child: Obx( - () => _isLoadingMoreItems.value + () => _isLoadingMoreItems.valueR ? const Padding( padding: EdgeInsets.all(8.0), child: Stack( diff --git a/lib/youtube/pages/yt_search_results_page.dart b/lib/youtube/pages/yt_search_results_page.dart index e25bd994..af0781e4 100644 --- a/lib/youtube/pages/yt_search_results_page.dart +++ b/lib/youtube/pages/yt_search_results_page.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; import 'package:namida/controller/connectivity.dart'; @@ -9,6 +8,7 @@ import 'package:namida/core/dimensions.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/packages/three_arched_circle.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/settings/extra_settings.dart'; @@ -140,18 +140,16 @@ class YoutubeSearchResultsPageState extends State with onTap: () { // if (_isLoadingLocalLookupList.value || currentSearchText == '') return; NamidaNavigator.inst.isytLocalSearchInFullPage = true; - NamidaNavigator.inst.ytLocalSearchNavigatorKey?.currentState?.push( - GetPageRoute( - transition: Transition.cupertino, - page: () => YTLocalSearchResults( - key: _offlineSearchPageKey, - initialSearch: currentSearchText, - onVideoTap: widget.onVideoTap, - onPopping: (didChangeSort) { - if (didChangeSort) setState(() {}); - }, - ), + NamidaNavigator.inst.ytLocalSearchNavigatorKey.currentState?.pushPage( + YTLocalSearchResults( + key: _offlineSearchPageKey, + initialSearch: currentSearchText, + onVideoTap: widget.onVideoTap, + onPopping: (didChangeSort) { + if (didChangeSort) setState(() {}); + }, ), + maintainState: false, ); }, child: Row( @@ -164,8 +162,9 @@ class YoutubeSearchResultsPageState extends State with ), const Spacer(), const SizedBox(width: 6.0), - Obx( - () => YTLocalSearchController.inst.didLoadLookupLists.value == false ? const LoadingIndicator() : const SizedBox(), + ObxO( + rx: YTLocalSearchController.inst.didLoadLookupLists, + builder: (didLoadLookupLists) => didLoadLookupLists == false ? const LoadingIndicator() : const SizedBox(), ), const SizedBox(width: 6.0), const Icon(Broken.arrow_right_3), @@ -222,7 +221,7 @@ class YoutubeSearchResultsPageState extends State with itemBuilder: (context, index) { final item = _searchResult[index]; switch (item.runtimeType) { - case StreamInfoItem: + case const (StreamInfoItem): return YoutubeVideoCard( thumbnailHeight: thumbnailHeight, thumbnailWidth: thumbnailWidth, @@ -231,13 +230,14 @@ class YoutubeSearchResultsPageState extends State with playlistID: null, onTap: widget.onVideoTap == null ? null : () => widget.onVideoTap!(item as StreamInfoItem), ); - case YoutubePlaylist: + case const (YoutubePlaylist): return YoutubePlaylistCard( playlist: item, + playOnTap: false, thumbnailHeight: thumbnailHeight, thumbnailWidth: thumbnailWidth, ); - case YoutubeChannel: + case const (YoutubeChannel): return YoutubeChannelCard( channel: item, thumbnailSize: context.width * 0.18, @@ -248,7 +248,7 @@ class YoutubeSearchResultsPageState extends State with ), SliverToBoxAdapter( child: Obx( - () => _isFetchingMoreResults.value + () => _isFetchingMoreResults.valueR ? const Padding( padding: EdgeInsets.all(8.0), child: Stack( diff --git a/lib/youtube/seek_ready_widget.dart b/lib/youtube/seek_ready_widget.dart index b0c08c6a..3a6a6c86 100644 --- a/lib/youtube/seek_ready_widget.dart +++ b/lib/youtube/seek_ready_widget.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:namida/controller/settings_controller.dart'; -import 'package:namida/core/enums.dart'; import 'package:vibration/vibration.dart'; import 'package:namida/controller/current_color.dart'; import 'package:namida/controller/miniplayer_controller.dart'; import 'package:namida/controller/player_controller.dart'; +import 'package:namida/controller/settings_controller.dart'; +import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; +import 'package:namida/core/utils.dart'; class SeekReadyDimensions { static const barHeight = 32.0; @@ -74,11 +74,11 @@ class _SeekReadyWidgetState extends State with SingleTickerProv _seekPercentage.value = percentageSwiped; } - Duration get _currentDuration => Player.inst.getCurrentVideoDuration; + Duration get _currentDurationR => Player.inst.getCurrentVideoDurationR; void _onSeekEnd() async { widget.onDraggingChange?.call(false); - final newSeek = _seekPercentage.value * (_currentDuration.inMilliseconds); + final newSeek = _seekPercentage.value * (Player.inst.getCurrentVideoDuration.inMilliseconds); await Player.inst.seek(Duration(milliseconds: newSeek.round())); } @@ -168,7 +168,7 @@ class _SeekReadyWidgetState extends State with SingleTickerProv if (_dragUpToCancel > _dragUpToCancelMax) { _canDragToSeekLatest = false; setState(() { - _currentSeekStuckWord = [" --:-- ", " kuru ", "umm.."].random; + _currentSeekStuckWord = [" --:-- ", " kuru ", "umm..", "🫵😂", "🫵😹"].random; _dragToSeek = false; }); Vibration.vibrate(duration: 20, amplitude: 80); @@ -228,13 +228,13 @@ class _SeekReadyWidgetState extends State with SingleTickerProv // -- current seek Obx( () { - final currentPositionMS = Player.inst.nowPlayingPosition; - final seekTo = _seekPercentage.value * _currentDuration.inMilliseconds; + final currentPositionMS = Player.inst.nowPlayingPositionR; + final seekTo = _seekPercentage.valueR * _currentDurationR.inMilliseconds; final seekToDiff = seekTo - currentPositionMS; final plusOrMinus = seekToDiff < 0 ? ' ' : '+'; final finalText = _currentSeekStuckWord != '' ? _currentSeekStuckWord : "$plusOrMinus${seekToDiff.round().milliSecondsLabel} "; return Transform.translate( - offset: Offset((maxWidth * _seekPercentage.value - seekTextWidth * 0.5).clamp(seekTextExtraMargin, maxWidth - seekTextWidth - seekTextExtraMargin), -12.0), + offset: Offset((maxWidth * _seekPercentage.valueR - seekTextWidth * 0.5).clamp(seekTextExtraMargin, maxWidth - seekTextWidth - seekTextExtraMargin), -12.0), child: AnimatedBuilder( animation: _animation, child: Container( @@ -272,11 +272,11 @@ class _SeekReadyWidgetState extends State with SingleTickerProv child: AnimatedBuilder( animation: _animation, child: Obx(() { - final durMS = Player.inst.getCurrentVideoDuration.inMilliseconds; - final currentPositionMS = Player.inst.nowPlayingPosition; - final buffered = Player.inst.buffered; - final videoCached = Player.inst.currentCachedVideo != null; - final audioCached = widget.isLocal || Player.inst.currentCachedAudio != null; + final durMS = Player.inst.getCurrentVideoDurationR.inMilliseconds; + final currentPositionMS = Player.inst.nowPlayingPositionR; + final buffered = Player.inst.buffered.valueR; + final videoCached = Player.inst.currentCachedVideo.valueR != null; + final audioCached = widget.isLocal || Player.inst.currentCachedAudio.valueR != null; return SizedBox( width: maxWidth, child: Stack( @@ -310,7 +310,7 @@ class _SeekReadyWidgetState extends State with SingleTickerProv ), child: SizedBox( width: maxWidth * - ((videoCached && audioCached) || (audioCached && Player.inst.isAudioOnlyPlayback) + ((videoCached && audioCached) || (audioCached && settings.ytIsAudioOnlyMode.valueR) ? 1.0 : buffered > Duration.zero && durMS > 0 ? buffered.inMilliseconds / durMS @@ -352,8 +352,8 @@ class _SeekReadyWidgetState extends State with SingleTickerProv animation: _animation, child: Obx( () { - final durMS = Player.inst.getCurrentVideoDuration.inMilliseconds; - final currentPositionMS = Player.inst.nowPlayingPosition; + final durMS = Player.inst.getCurrentVideoDurationR.inMilliseconds; + final currentPositionMS = Player.inst.nowPlayingPositionR; final pos = durMS == 0 ? 0 : (maxWidth * (currentPositionMS / durMS)); final clampedEdge = clampCircleEdges ? halfCircle / 2 : 0; return Transform.translate( @@ -388,8 +388,9 @@ class _SeekReadyWidgetState extends State with SingleTickerProv Obx( () { final clampedEdge = clampCircleEdges ? circleWidth / 2 : 0; + final seekP = _seekPercentage.valueR; return Transform.translate( - offset: Offset(-halfCircle + (maxWidth * _seekPercentage.value).clamp(clampedEdge, maxWidth - clampedEdge), barHeight / 4), + offset: Offset(-halfCircle + (maxWidth * seekP).clamp(clampedEdge, maxWidth - clampedEdge), barHeight / 4), child: AnimatedBuilder( animation: _animation, child: Container( diff --git a/lib/youtube/widgets/yt_action_button.dart b/lib/youtube/widgets/yt_action_button.dart index 67e68068..bf469429 100644 --- a/lib/youtube/widgets/yt_action_button.dart +++ b/lib/youtube/widgets/yt_action_button.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/widgets/yt_shimmer.dart'; diff --git a/lib/youtube/widgets/yt_card.dart b/lib/youtube/widgets/yt_card.dart index 667ee5c3..c771655d 100644 --- a/lib/youtube/widgets/yt_card.dart +++ b/lib/youtube/widgets/yt_card.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/core/dimensions.dart'; -import 'package:namida/core/extensions.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/widgets/yt_shimmer.dart'; import 'package:namida/youtube/widgets/yt_thumbnail.dart'; @@ -26,7 +25,6 @@ class YoutubeCard extends StatelessWidget { final double thumbnailWidthPercentage; final IconData? smallBoxIcon; final bool extractColor; - final List Function()? menuChildren; final List Function()? menuChildrenDefault; final bool isCircle; final List bottomRightWidgets; @@ -56,7 +54,6 @@ class YoutubeCard extends StatelessWidget { this.thumbnailWidthPercentage = 1.0, this.smallBoxIcon, this.extractColor = false, - this.menuChildren, this.menuChildrenDefault, this.isCircle = false, this.bottomRightWidgets = const [], @@ -123,7 +120,7 @@ class YoutubeCard extends StatelessWidget { shimmerEnabled: shimmerEnabled && title == '', child: Text( title, - style: context.textTheme.displayMedium?.copyWith(fontSize: 13.0.multipliedFontScale * fontMultiplier), + style: context.textTheme.displayMedium?.copyWith(fontSize: 13.0 * fontMultiplier), maxLines: 2, overflow: TextOverflow.ellipsis, ), @@ -138,7 +135,7 @@ class YoutubeCard extends StatelessWidget { subtitle, style: context.textTheme.displaySmall?.copyWith( fontWeight: FontWeight.w400, - fontSize: 13.0.multipliedFontScale * fontMultiplier, + fontSize: 13.0 * fontMultiplier, ), maxLines: 2, overflow: TextOverflow.ellipsis, @@ -180,7 +177,7 @@ class YoutubeCard extends StatelessWidget { thirdLineText, style: context.textTheme.displaySmall?.copyWith( fontWeight: FontWeight.w400, - fontSize: 11.0.multipliedFontScale * fontMultiplier, + fontSize: 11.0 * fontMultiplier, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -212,12 +209,11 @@ class YoutubeCard extends StatelessWidget { children: bottomRightWidgets, ), ), - if (!shimmerEnabled && ((menuChildren?.call().isNotEmpty ?? false) || (menuChildrenDefault?.call().isNotEmpty ?? false))) + if (!shimmerEnabled && menuChildrenDefault != null) Positioned( top: 0.0, right: 0.0, child: NamidaPopupWrapper( - children: menuChildren, childrenDefault: menuChildrenDefault, child: const Padding( padding: EdgeInsets.all(8.0), diff --git a/lib/youtube/widgets/yt_channel_card.dart b/lib/youtube/widgets/yt_channel_card.dart index f66704a0..47be028f 100644 --- a/lib/youtube/widgets/yt_channel_card.dart +++ b/lib/youtube/widgets/yt_channel_card.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; import 'package:namida/controller/navigator_controller.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/pages/yt_channel_subpage.dart'; import 'package:namida/youtube/widgets/yt_shimmer.dart'; diff --git a/lib/youtube/widgets/yt_comment_card.dart b/lib/youtube/widgets/yt_comment_card.dart index fd987d6c..3d42c2b0 100644 --- a/lib/youtube/widgets/yt_comment_card.dart +++ b/lib/youtube/widgets/yt_comment_card.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; import 'package:selectable_autolink_text/selectable_autolink_text.dart'; @@ -10,6 +9,7 @@ import 'package:namida/core/constants.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; import 'package:namida/youtube/controller/youtube_subscriptions_controller.dart'; @@ -93,7 +93,7 @@ class YTCommentCard extends StatelessWidget { Text( lang.PINNED, style: context.textTheme.displaySmall?.copyWith( - fontSize: 11.5.multipliedFontScale, + fontSize: 11.5, ), ), ], @@ -113,7 +113,7 @@ class YTCommentCard extends StatelessWidget { author, if (uploadedFrom != null) uploadedFrom, ].join(' • '), - style: context.textTheme.displaySmall?.copyWith(fontWeight: FontWeight.w400, color: context.theme.colorScheme.onBackground.withAlpha(180)), + style: context.textTheme.displaySmall?.copyWith(fontWeight: FontWeight.w400, color: context.theme.colorScheme.onSurface.withAlpha(180)), ), ), if (isHearted) ...[ @@ -160,18 +160,18 @@ class YTCommentCard extends StatelessWidget { text, maxLines: lines, style: context.textTheme.displaySmall?.copyWith( - fontSize: 13.5.multipliedFontScale, + fontSize: 13.5, fontWeight: FontWeight.w500, - color: context.theme.colorScheme.onBackground.withAlpha(220), + color: context.theme.colorScheme.onSurface.withAlpha(220), ), linkStyle: context.textTheme.displayMedium?.copyWith( color: context.theme.colorScheme.primary.withAlpha(210), - fontSize: 13.5.multipliedFontScale, + fontSize: 13.5, ), highlightedLinkStyle: TextStyle( color: context.theme.colorScheme.primary.withAlpha(220), - backgroundColor: context.theme.colorScheme.onBackground.withAlpha(40), - fontSize: 13.5.multipliedFontScale, + backgroundColor: context.theme.colorScheme.onSurface.withAlpha(40), + fontSize: 13.5, ), scrollPhysics: const NeverScrollableScrollPhysics(), linkRegExpPattern: NamidaLinkRegex.all, @@ -236,11 +236,11 @@ class YTCommentCard extends StatelessWidget { child: TextButton.icon( style: TextButton.styleFrom( visualDensity: VisualDensity.compact, - foregroundColor: context.theme.colorScheme.onBackground.withAlpha(200), + foregroundColor: context.theme.colorScheme.onSurface.withAlpha(200), ), onPressed: () {}, icon: const Icon(Broken.document, size: 16.0), - label: Text( + label: NamidaButtonText( [ lang.REPLIES, if (repliesCount != null) repliesCount, @@ -351,9 +351,9 @@ class YTCommentCardCompact extends StatelessWidget { if (uploadedFrom != null) uploadedFrom, ].join(' • '), style: context.textTheme.displaySmall?.copyWith( - fontSize: 11.5.multipliedFontScale, + fontSize: 11.5, fontWeight: FontWeight.w400, - color: context.theme.colorScheme.onBackground.withAlpha(180), + color: context.theme.colorScheme.onSurface.withAlpha(180), ), ), ), @@ -401,9 +401,9 @@ class YTCommentCardCompact extends StatelessWidget { maxLines: 3, overflow: TextOverflow.ellipsis, style: context.textTheme.displaySmall?.copyWith( - fontSize: 12.5.multipliedFontScale, + fontSize: 12.5, fontWeight: FontWeight.w500, - color: context.theme.colorScheme.onBackground.withAlpha(220), + color: context.theme.colorScheme.onSurface.withAlpha(220), ), ), ), @@ -422,7 +422,7 @@ class YTCommentCardCompact extends StatelessWidget { child: Text( likeCount?.formatDecimalShort() ?? '?', style: context.textTheme.displaySmall?.copyWith( - fontSize: 11.5.multipliedFontScale, + fontSize: 11.5, fontWeight: FontWeight.w400, ), ), @@ -439,7 +439,7 @@ class YTCommentCardCompact extends StatelessWidget { repliesCount, ].join(' • '), style: context.textTheme.displaySmall?.copyWith( - fontSize: 11.5.multipliedFontScale, + fontSize: 11.5, fontWeight: FontWeight.w400, ), ), diff --git a/lib/youtube/widgets/yt_download_task_item_card.dart b/lib/youtube/widgets/yt_download_task_item_card.dart index ef010b2e..857ed36e 100644 --- a/lib/youtube/widgets/yt_download_task_item_card.dart +++ b/lib/youtube/widgets/yt_download_task_item_card.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; -import 'package:get/get.dart'; import 'package:jiffy/jiffy.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; @@ -17,6 +16,7 @@ import 'package:namida/core/functions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/dialogs/track_info_dialog.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/settings/extra_settings.dart'; @@ -51,14 +51,14 @@ class YTDownloadTaskItemCard extends StatelessWidget { void Function()? onTap, Widget? Function(double size)? iconWidget, }) { - final textWidget = RichText( - text: TextSpan( + final textWidget = Text.rich( + TextSpan( children: [ - TextSpan(text: title, style: context.textTheme.displayMedium?.copyWith(fontSize: 13.0.multipliedFontScale)), + TextSpan(text: title, style: context.textTheme.displayMedium?.copyWith(fontSize: 13.0)), if (betweenBrackets != '') TextSpan( text: " ($betweenBrackets)", - style: Get.textTheme.displaySmall?.copyWith(fontSize: 11.0.multipliedFontScale), + style: namida.textTheme.displaySmall?.copyWith(fontSize: 11.0), ), ], ), @@ -133,13 +133,13 @@ class YTDownloadTaskItemCard extends StatelessWidget { style: { '*': Style.fromTextStyle( context.textTheme.displaySmall!.copyWith( - fontSize: 13.0.multipliedFontScale, + fontSize: 13.0, ), ), 'a': Style.fromTextStyle( context.textTheme.displaySmall!.copyWith( color: context.theme.colorScheme.primary.withAlpha(210), - fontSize: 12.5.multipliedFontScale, + fontSize: 12.5, ), ) }, @@ -261,7 +261,7 @@ class YTDownloadTaskItemCard extends StatelessWidget { .addSeparators( separator: NamidaContainerDivider( height: 1.5, - colorForce: context.theme.colorScheme.onBackground.withOpacity(0.2), + colorForce: context.theme.colorScheme.onSurface.withOpacity(0.2), ), skipFirst: 1, ) @@ -309,7 +309,7 @@ class YTDownloadTaskItemCard extends StatelessWidget { Expanded( child: Text( texts.joinText(), - style: context.textTheme.displaySmall?.copyWith(fontSize: 12.0.multipliedFontScale), + style: context.textTheme.displaySmall?.copyWith(fontSize: 12.0), ), ), ], @@ -381,8 +381,8 @@ class YTDownloadTaskItemCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 12.0), - RichText( - text: TextSpan( + Text.rich( + TextSpan( children: [ TextSpan(text: "$operationTitle: ", style: context.textTheme.displayLarge), TextSpan( @@ -611,7 +611,7 @@ class YTDownloadTaskItemCard extends StatelessWidget { const SizedBox(height: 4.0), Text( [percentageText, downloadInfoText].joinText(), - style: context.textTheme.displaySmall?.copyWith(fontSize: 11.0.multipliedFontScale), + style: context.textTheme.displaySmall?.copyWith(fontSize: 11.0), ), ], const SizedBox(height: 4.0), @@ -735,7 +735,7 @@ class YTDownloadTaskItemCard extends StatelessWidget { item.videoStream?.resolution, downloadedFile.fileSizeFormatted(), ].joinText(), - style: context.textTheme.displaySmall?.copyWith(fontSize: 11.0.multipliedFontScale), + style: context.textTheme.displaySmall?.copyWith(fontSize: 11.0), ), const SizedBox(width: 4.0), if (itemIcon != null) diff --git a/lib/youtube/widgets/yt_history_video_card.dart b/lib/youtube/widgets/yt_history_video_card.dart index a9fef4ba..4ab559a0 100644 --- a/lib/youtube/widgets/yt_history_video_card.dart +++ b/lib/youtube/widgets/yt_history_video_card.dart @@ -1,14 +1,15 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:jiffy/jiffy.dart'; import 'package:playlist_manager/module/playlist_id.dart'; +import 'package:namida/class/track.dart'; import 'package:namida/controller/current_color.dart'; import 'package:namida/controller/player_controller.dart'; import 'package:namida/core/dimensions.dart'; import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/pages/subpages/playlist_tracks_subpage.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/class/youtube_id.dart'; @@ -18,7 +19,7 @@ import 'package:namida/youtube/widgets/yt_thumbnail.dart'; import 'package:namida/youtube/yt_utils.dart'; class YTHistoryVideoCard extends StatelessWidget { - final List videos; + final List videos; final int? day; final int index; final List overrideListens; @@ -68,7 +69,7 @@ class YTHistoryVideoCard extends StatelessWidget { @override Widget build(BuildContext context) { final index = reversedList ? videos.length - 1 - this.index : this.index; - final video = videos[index]; + final video = videos[index] as YoutubeID; final thumbHeight = thumbnailHeight ?? (minimalCard ? 24.0 * 3.2 : Dimensions.youtubeCardItemHeight); final thumbWidth = minimalCardWidth ?? thumbHeight * 16 / 9; @@ -86,7 +87,6 @@ class YTHistoryVideoCard extends StatelessWidget { final draggingThumbWidget = draggableThumbnail && draggingEnabled ? NamidaReordererableListener( durationMs: 80, - isInQueue: true, index: index, child: Container( color: Colors.transparent, @@ -110,15 +110,20 @@ class YTHistoryVideoCard extends StatelessWidget { ), child: Obx( () { - final isCurrentlyPlaying = Player.inst.nowPlayingVideoID == video; - final sameDay = day == YoutubeHistoryController.inst.dayOfHighLight.value; - final sameIndex = index == YoutubeHistoryController.inst.indexToHighlight.value; - final hightlightedColor = sameDay && sameIndex ? context.theme.colorScheme.onBackground.withAlpha(40) : null; + bool willSleepAfterThis = false; + if (fromPlayerQueue) { + final sleepconfig = Player.inst.sleepTimerConfig.valueR; + willSleepAfterThis = sleepconfig.enableSleepAfterItems && Player.inst.sleepingItemIndex(sleepconfig.sleepAfterItems, Player.inst.currentIndex.valueR) == index; + } + + final isCurrentlyPlaying = Player.inst.currentVideoR == video; + final sameDay = day == YoutubeHistoryController.inst.dayOfHighLight.valueR; + final sameIndex = index == YoutubeHistoryController.inst.indexToHighlight.valueR; + final hightlightedColor = sameDay && sameIndex ? context.theme.colorScheme.onSurface.withAlpha(40) : null; final itemsColor7 = isCurrentlyPlaying ? Colors.white.withOpacity(0.7) : null; final itemsColor6 = isCurrentlyPlaying ? Colors.white.withOpacity(0.6) : null; final itemsColor5 = isCurrentlyPlaying ? Colors.white.withOpacity(0.5) : null; final threeLines = draggableThumbnail ? ThreeLineSmallContainers(enabled: draggingEnabled, color: itemsColor5) : null; - final willSleepAfterThis = fromPlayerQueue && Player.inst.enableSleepAfterTracks && Player.inst.sleepingTrackIndex == index; final children = [ if (threeLines != null) draggingBarsBuilder?.call(itemsColor5) ?? threeLines, SizedBox( @@ -156,7 +161,7 @@ class YTHistoryVideoCard extends StatelessWidget { maxLines: minimalCard ? 1 : 2, overflow: TextOverflow.ellipsis, style: context.textTheme.displayMedium?.copyWith( - fontSize: minimalCard ? 12.0.multipliedFontScale : null, + fontSize: minimalCard ? 12.0 : null, color: itemsColor7, ), ), @@ -166,7 +171,7 @@ class YTHistoryVideoCard extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: context.textTheme.displaySmall?.copyWith( - fontSize: minimalCard ? 11.5.multipliedFontScale : null, + fontSize: minimalCard ? 11.5 : null, color: itemsColor6, ), ), @@ -176,7 +181,7 @@ class YTHistoryVideoCard extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: context.textTheme.displaySmall?.copyWith( - fontSize: minimalCard ? 11.0.multipliedFontScale : null, + fontSize: minimalCard ? 11.0 : null, color: itemsColor5, ), ), @@ -193,20 +198,25 @@ class YTHistoryVideoCard extends StatelessWidget { YTUtils.expandMiniplayer(); if (fromPlayerQueue) { final i = this.index; - if (i == Player.inst.currentIndex) { + if (i == Player.inst.currentIndex.value) { Player.inst.togglePlayPause(); } else { Player.inst.skipToQueueItem(this.index); } } else { Player.inst.playOrPause( - this.index, (reversedList ? videos.reversed : videos).map((e) => YoutubeID(id: e.id, watchNull: e.watchNull, playlistID: playlistID)), QueueSource.others); + this.index, + (reversedList ? videos.reversed : videos).map((e) { + e as YoutubeID; + return YoutubeID(id: e.id, watchNull: e.watchNull, playlistID: playlistID); + }), + QueueSource.others); } }, height: minimalCard ? null : Dimensions.youtubeCardItemExtent, margin: EdgeInsets.symmetric(horizontal: minimalCard ? 2.0 : 4.0, vertical: Dimensions.youtubeCardItemVerticalPadding), bgColor: isCurrentlyPlaying - ? (fromPlayerQueue ? CurrentColor.inst.miniplayerColor : CurrentColor.inst.color).withAlpha(140) + ? (fromPlayerQueue ? CurrentColor.inst.miniplayerColor : CurrentColor.inst.currentColorScheme).withAlpha(140) : (hightlightedColor ?? context.theme.cardColor.withOpacity(cardColorOpacity)), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12.0.multipliedRadius), diff --git a/lib/youtube/widgets/yt_playlist_card.dart b/lib/youtube/widgets/yt_playlist_card.dart index d3d5ca72..2c9dc484 100644 --- a/lib/youtube/widgets/yt_playlist_card.dart +++ b/lib/youtube/widgets/yt_playlist_card.dart @@ -6,6 +6,7 @@ import 'package:namida/controller/player_controller.dart'; import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; +import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/functions/yt_playlist_utils.dart'; import 'package:namida/youtube/pages/yt_playlist_subpage.dart'; import 'package:namida/youtube/widgets/yt_card.dart'; @@ -24,6 +25,14 @@ class YoutubePlaylistCard extends StatelessWidget { this.playOnTap = false, }); + List getMenuItems(BuildContext context, YoutubePlaylist playlist) { + return playlist.getPopupMenuItems( + context, + displayPlay: playOnTap == false, + playlistToOpen: playOnTap ? playlist : null, + ); + } + @override Widget build(BuildContext context) { final count = playlist?.streamCount; @@ -31,41 +40,40 @@ class YoutubePlaylistCard extends StatelessWidget { final thumbnailUrl = playlist?.thumbnailUrl; final firstVideoID = playlist?.streams.firstOrNull?.id; final goodVideoID = firstVideoID != null && firstVideoID != ''; - return YoutubeCard( - thumbnailHeight: thumbnailHeight, - thumbnailWidth: thumbnailWidth, - isPlaylist: true, - isImageImportantInCache: false, - extractColor: true, - borderRadius: 12.0, - videoId: goodVideoID ? firstVideoID : null, - thumbnailUrl: goodVideoID ? null : thumbnailUrl, - shimmerEnabled: playlist == null, - title: playlist?.name ?? '', - subtitle: playlist?.uploaderName ?? '', - thirdLineText: '', - onTap: () async { - if (playlist != null) { - if (playOnTap) { - final videos = await playlist!.fetchAllPlaylistAsYTIDs(context: context); - if (videos.isEmpty) return; - Player.inst.playOrPause(0, videos, QueueSource.others); - } else { - NamidaNavigator.inst.navigateTo(YTHostedPlaylistSubpage(playlist: playlist!)); + return NamidaPopupWrapper( + openOnTap: false, + openOnLongPress: true, + childrenDefault: playlist == null ? null : () => getMenuItems(context, playlist!), + child: YoutubeCard( + thumbnailHeight: thumbnailHeight, + thumbnailWidth: thumbnailWidth, + isPlaylist: true, + isImageImportantInCache: false, + extractColor: true, + borderRadius: 12.0, + videoId: goodVideoID ? firstVideoID : null, + thumbnailUrl: goodVideoID ? null : thumbnailUrl, + shimmerEnabled: playlist == null, + title: playlist?.name ?? '', + subtitle: playlist?.uploaderName ?? '', + thirdLineText: '', + onTap: () async { + if (playlist != null) { + if (playOnTap) { + final videos = await playlist!.fetchAllPlaylistAsYTIDs(context: context); + if (videos.isEmpty) return; + Player.inst.playOrPause(0, videos, QueueSource.others); + } else { + NamidaNavigator.inst.navigateTo(YTHostedPlaylistSubpage(playlist: playlist!)); + } } - } - }, - displayChannelThumbnail: false, - displaythirdLineText: false, - smallBoxText: countText, - smallBoxIcon: Broken.play_cricle, - menuChildrenDefault: () => - playlist?.getPopupMenuItems( - context, - displayPlay: playOnTap == false, - playlistToOpen: playOnTap ? playlist : null, - ) ?? - [], + }, + displayChannelThumbnail: false, + displaythirdLineText: false, + smallBoxText: countText, + smallBoxIcon: Broken.play_cricle, + menuChildrenDefault: playlist == null ? null : () => getMenuItems(context, playlist!), + ), ); } } diff --git a/lib/youtube/widgets/yt_queue_chip.dart b/lib/youtube/widgets/yt_queue_chip.dart index d3348f1b..a912f83f 100644 --- a/lib/youtube/widgets/yt_queue_chip.dart +++ b/lib/youtube/widgets/yt_queue_chip.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/controller/miniplayer_controller.dart'; import 'package:namida/controller/navigator_controller.dart'; @@ -10,8 +9,10 @@ import 'package:namida/core/functions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/packages/scroll_physics_modified.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; +import 'package:namida/youtube/class/youtube_id.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; import 'package:namida/youtube/controller/yt_generators_controller.dart'; import 'package:namida/youtube/functions/add_to_playlist_sheet.dart'; @@ -113,7 +114,7 @@ class YTMiniplayerQueueChipState extends State with Ticke NamidaNavigator.inst.isQueueSheetOpen = false; } - double get _itemScrollOffsetInQueue => Dimensions.youtubeCardItemExtent * Player.inst.currentIndex - _screenHeight * 0.3; + double get _itemScrollOffsetInQueue => Dimensions.youtubeCardItemExtent * Player.inst.currentIndex.value - _screenHeight * 0.3; void _animateQueueToCurrentTrack({bool jump = false, bool minZero = false}) { if (_queueScrollController.hasClients) { @@ -146,9 +147,9 @@ class YTMiniplayerQueueChipState extends State with Ticke alignment: Alignment.bottomCenter, fit: StackFit.expand, children: [ - Obx( - () { - final queue = Player.inst.currentQueueYoutube; + ObxO( + rx: Player.inst.currentQueue, + builder: (queue) { final isSingle = queue.length == 1; return Positioned( bottom: 0, @@ -201,18 +202,18 @@ class YTMiniplayerQueueChipState extends State with Ticke Expanded( child: Obx( () { - final nextItem = Player.inst.currentQueueYoutube.length - 1 >= Player.inst.currentIndex + 1 - ? Player.inst.currentQueueYoutube[Player.inst.currentIndex + 1] - : null; + final currentIndex = Player.inst.currentIndex.valueR; + final nextItem = + Player.inst.currentQueue.valueR.length - 1 >= currentIndex + 1 ? Player.inst.currentQueue.valueR[currentIndex + 1] as YoutubeID : null; final nextItemName = nextItem == null ? '' : YoutubeController.inst.getVideoName(nextItem.id); - + final queueLength = Player.inst.currentQueue.valueR.length; return Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "${Player.inst.currentIndex + 1}/${Player.inst.currentQueueYoutube.length}", + "${currentIndex + 1}/$queueLength", style: context.textTheme.displaySmall?.copyWith(fontWeight: FontWeight.w600), ), // const SizedBox(height: 2.0), @@ -306,7 +307,7 @@ class YTMiniplayerQueueChipState extends State with Ticke ), Obx( () => Text( - "${Player.inst.currentIndex + 1}/${Player.inst.currentQueueYoutube.length}", + "${Player.inst.currentIndex.valueR + 1}/${Player.inst.currentQueue.valueR.length}", style: context.textTheme.displaySmall?.copyWith(fontWeight: FontWeight.w600), ), ), @@ -319,7 +320,7 @@ class YTMiniplayerQueueChipState extends State with Ticke tooltip: lang.ADD_TO_PLAYLIST, onTap: () { showAddToPlaylistSheet( - ids: Player.inst.currentQueueYoutube.map((e) => e.id), + ids: Player.inst.currentQueue.value.mapAs().map((e) => e.id), idsNamesLookup: const {}, ); }, @@ -331,7 +332,7 @@ class YTMiniplayerQueueChipState extends State with Ticke onTap: () { NamidaNavigator.inst.navigateTo( YTPlaylistDownloadPage( - ids: Player.inst.currentQueueYoutube, + ids: Player.inst.currentQueue.value.mapAs().toList(), playlistName: lang.QUEUE, infoLookup: const {}, ), @@ -351,35 +352,29 @@ class YTMiniplayerQueueChipState extends State with Ticke Expanded( child: Obx( () { - final queue = Player.inst.currentQueueYoutube; - final canScroll = _canScrollQueue.value; + final queue = Player.inst.currentQueue.valueR; + final canScroll = _canScrollQueue.valueR; return IgnorePointer( ignoring: !canScroll, child: NamidaListView( padding: EdgeInsets.zero, scrollController: _queueScrollController, itemCount: queue.length, - itemExtents: List.filled(queue.length, Dimensions.youtubeCardItemExtent), + itemExtent: Dimensions.youtubeCardItemExtent, onReorderStart: (index) => MiniPlayerController.inst.invokeStartReordering(), onReorderEnd: (index) => MiniPlayerController.inst.invokeDoneReordering(), onReorder: (oldIndex, newIndex) => Player.inst.reorderTrack(oldIndex, newIndex), physics: canScroll ? const ClampingScrollPhysicsModified() : const NeverScrollableScrollPhysics(), itemBuilder: (context, i) { - final video = queue[i]; + final video = queue[i] as YoutubeID; return FadeDismissible( key: Key("Diss_${video.id}_$i"), onDismissed: (direction) { Player.inst.removeFromQueue(i); MiniPlayerController.inst.invokeDoneReordering(); }, - onUpdate: (details) { - final isReordering = details.progress != 0.0; - if (isReordering) { - MiniPlayerController.inst.invokeStartReordering(); - } else { - MiniPlayerController.inst.invokeDoneReordering(); - } - }, + onDismissStart: (_) => MiniPlayerController.inst.invokeStartReordering(), + onDismissEnd: (_) => MiniPlayerController.inst.invokeDoneReordering(), child: YTHistoryVideoCard( key: Key("${i}_${video.id}"), videos: queue, @@ -413,10 +408,11 @@ class YTMiniplayerQueueChipState extends State with Ticke child: QueueUtilsRow( itemsKeyword: (number) => number.displayVideoKeyword, onAddItemsTap: () => TracksAddOnTap().onAddVideosTap(context), - scrollQueueWidget: Obx( - () => NamidaButton( + scrollQueueWidget: ObxO( + rx: _arrowIcon, + builder: (arrowIcon) => NamidaButton( onPressed: _animateQueueToCurrentTrack, - icon: _arrowIcon.value, + icon: arrowIcon, ), ), ), @@ -470,7 +466,7 @@ class _ActionItem extends StatelessWidget { style: ButtonStyle( tapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: const VisualDensity(horizontal: -2.0, vertical: -2.0), - backgroundColor: MaterialStatePropertyAll(context.theme.colorScheme.secondary.withOpacity(0.18)), + backgroundColor: WidgetStatePropertyAll(context.theme.colorScheme.secondary.withOpacity(0.18)), ), onPressed: onTap, icon: Icon(icon, size: 20.0), diff --git a/lib/youtube/widgets/yt_shimmer.dart b/lib/youtube/widgets/yt_shimmer.dart index 0d620c43..98c0bff9 100644 --- a/lib/youtube/widgets/yt_shimmer.dart +++ b/lib/youtube/widgets/yt_shimmer.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/core/extensions.dart'; +import 'package:namida/core/utils.dart'; class NamidaDummyContainer extends StatelessWidget { final double? width; @@ -30,7 +30,7 @@ class NamidaDummyContainer extends StatelessWidget { height: height, clipBehavior: Clip.antiAlias, decoration: BoxDecoration( - color: context.theme.colorScheme.background, + color: context.theme.colorScheme.surface, borderRadius: isCircle ? null : BorderRadius.circular(borderRadius.multipliedRadius), shape: isCircle ? BoxShape.circle : BoxShape.rectangle, ), diff --git a/lib/youtube/widgets/yt_subscribe_buttons.dart b/lib/youtube/widgets/yt_subscribe_buttons.dart index 3d2345ec..1b5b1d1f 100644 --- a/lib/youtube/widgets/yt_subscribe_buttons.dart +++ b/lib/youtube/widgets/yt_subscribe_buttons.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; +import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/controller/youtube_subscriptions_controller.dart'; class YTSubscribeButton extends StatelessWidget { @@ -27,7 +28,7 @@ class YTSubscribeButton extends StatelessWidget { children: [ Icon(subscribed ? Broken.tick_square : Broken.video, size: 20.0), const SizedBox(width: 8.0), - Text( + NamidaButtonText( subscribed ? lang.SUBSCRIBED : lang.SUBSCRIBE, ), ], diff --git a/lib/youtube/widgets/yt_thumbnail.dart b/lib/youtube/widgets/yt_thumbnail.dart index 9a549d51..bf870d69 100644 --- a/lib/youtube/widgets/yt_thumbnail.dart +++ b/lib/youtube/widgets/yt_thumbnail.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/base/loading_items_delay.dart'; import 'package:namida/class/color_m.dart'; @@ -263,7 +263,7 @@ class _YoutubeThumbnailState extends State with LoadingItemsDe ), ), Obx( - () => _thumbnailNotFound.value + () => _thumbnailNotFound.valueR ? Positioned( top: 0.0, right: 0.0, diff --git a/lib/youtube/widgets/yt_videos_actions_bar.dart b/lib/youtube/widgets/yt_videos_actions_bar.dart index d0513245..57254e7d 100644 --- a/lib/youtube/widgets/yt_videos_actions_bar.dart +++ b/lib/youtube/widgets/yt_videos_actions_bar.dart @@ -97,7 +97,7 @@ class YTVideosActionBar extends StatelessWidget { void _onAddToPlaylist() { final ids = []; final info = {}; - videos.loop((e, index) { + videos.loop((e) { final id = e.id; ids.add(id); info[id] = infoLookup[id]?.name; diff --git a/lib/youtube/youtube_miniplayer.dart b/lib/youtube/youtube_miniplayer.dart index 4939d1f6..1c67d193 100644 --- a/lib/youtube/youtube_miniplayer.dart +++ b/lib/youtube/youtube_miniplayer.dart @@ -1,7 +1,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; -import 'package:get/get.dart'; import 'package:jiffy/jiffy.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; import 'package:share_plus/share_plus.dart'; @@ -20,6 +19,7 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/packages/mp.dart'; import 'package:namida/packages/scroll_physics_modified.dart'; import 'package:namida/packages/three_arched_circle.dart'; @@ -94,1099 +94,1100 @@ class _YoutubeMiniPlayerState extends State { return DefaultTextStyle( style: context.textTheme.displayMedium!, - child: Obx( - () { - final videoInfo = YoutubeController.inst.currentYoutubeMetadataVideo.value ?? Player.inst.currentVideoInfo; - final videoChannel = YoutubeController.inst.currentYoutubeMetadataChannel.value; + child: ObxO( + rx: YoutubePlaylistController.inst.favouritesPlaylist, + builder: (favouritesPlaylist) { + return Obx( + () { + final videoInfo = YoutubeController.inst.currentYoutubeMetadataVideo.valueR ?? Player.inst.currentVideoInfo.valueR; + final videoChannel = YoutubeController.inst.currentYoutubeMetadataChannel.valueR; - String? uploadDate; - String? uploadDateAgo; + String? uploadDate; + String? uploadDateAgo; - final parsedDate = videoInfo?.date ?? Player.inst.currentVideoInfo?.date; + final parsedDate = videoInfo?.date ?? Player.inst.currentVideoInfo.valueR?.date; - if (parsedDate != null) { - uploadDate = parsedDate.millisecondsSinceEpoch.dateFormattedOriginal; - uploadDateAgo = Jiffy.parseFromDateTime(parsedDate).fromNow(); - } + if (parsedDate != null) { + uploadDate = parsedDate.millisecondsSinceEpoch.dateFormattedOriginal; + uploadDateAgo = Jiffy.parseFromDateTime(parsedDate).fromNow(); + } - final miniTitle = videoInfo?.name; - final miniSubtitle = videoChannel?.name ?? videoInfo?.uploaderName; - final currentId = Player.inst.getCurrentVideoId; + final miniTitle = videoInfo?.name; + final miniSubtitle = videoChannel?.name ?? videoInfo?.uploaderName; + final currentId = Player.inst.getCurrentVideoIdR; - final channelName = videoChannel?.name ?? videoInfo?.uploaderName; - final channelThumbnail = videoChannel?.avatarUrl ?? videoInfo?.uploaderAvatarUrl; - final channelIsVerified = videoChannel?.isVerified ?? videoInfo?.isUploaderVerified ?? false; - final channelSubs = videoChannel?.subscriberCount ?? Player.inst.currentChannelInfo?.subscriberCount; - final channelIDOrURL = videoChannel?.id ?? videoInfo?.uploaderUrl ?? Player.inst.currentChannelInfo?.id; + final channelName = videoChannel?.name ?? videoInfo?.uploaderName; + final channelThumbnail = videoChannel?.avatarUrl ?? videoInfo?.uploaderAvatarUrl; + final channelIsVerified = videoChannel?.isVerified ?? videoInfo?.isUploaderVerified ?? false; + final channelSubs = videoChannel?.subscriberCount ?? Player.inst.currentChannelInfo.valueR?.subscriberCount; + final channelIDOrURL = videoChannel?.id ?? videoInfo?.uploaderUrl ?? Player.inst.currentChannelInfo.valueR?.id; - final isUserLiked = YoutubePlaylistController.inst.favouritesPlaylist.value.tracks.firstWhereEff((element) => element.id == currentId) != null; + final isUserLiked = favouritesPlaylist.tracks.firstWhereEff((element) => element.id == currentId) != null; - final videoLikeCount = (isUserLiked ? 1 : 0) + (videoInfo?.likeCount ?? Player.inst.currentVideoInfo?.likeCount ?? 0); - final videoDislikeCount = videoInfo?.dislikeCount ?? Player.inst.currentVideoInfo?.dislikeCount; - final videoViewCount = videoInfo?.viewCount ?? Player.inst.currentVideoInfo?.viewCount; + final videoLikeCount = (isUserLiked ? 1 : 0) + (videoInfo?.likeCount ?? Player.inst.currentVideoInfo.valueR?.likeCount ?? 0); + final videoDislikeCount = videoInfo?.dislikeCount ?? Player.inst.currentVideoInfo.valueR?.dislikeCount; + final videoViewCount = videoInfo?.viewCount ?? Player.inst.currentVideoInfo.valueR?.viewCount; - final description = videoInfo?.description; - final descriptionWidget = description == null || description == '' - ? null - : SelectionArea( - child: Html( - data: description, - style: { - '*': Style.fromTextStyle( - context.textTheme.displayMedium!.copyWith( - fontSize: 14.0.multipliedFontScale, - ), - ), - 'a': Style.fromTextStyle( - context.textTheme.displayMedium!.copyWith( - color: context.theme.colorScheme.primary.withAlpha(210), - fontSize: 13.5.multipliedFontScale, - ), - ) - }, - onLinkTap: (url, attributes, element) async { - if (url != null) { - final partsDur = url.split("$currentId&t="); - if (partsDur.length > 1) { - try { - await Player.inst.seek(Duration(seconds: int.parse(partsDur.last))); - } catch (e) { - snackyy(title: lang.ERROR, message: e.toString(), isError: true, top: false); + final description = videoInfo?.description; + final descriptionWidget = description == null || description == '' + ? null + : SelectionArea( + child: Html( + data: description, + style: { + '*': Style.fromTextStyle( + context.textTheme.displayMedium!.copyWith( + fontSize: 14.0, + ), + ), + 'a': Style.fromTextStyle( + context.textTheme.displayMedium!.copyWith( + color: context.theme.colorScheme.primary.withAlpha(210), + fontSize: 13.5, + ), + ) + }, + onLinkTap: (url, attributes, element) async { + if (url != null) { + final partsDur = url.split("$currentId&t="); + if (partsDur.length > 1) { + try { + await Player.inst.seek(Duration(seconds: int.parse(partsDur.last))); + } catch (e) { + snackyy(title: lang.ERROR, message: e.toString(), isError: true, top: false); + } + } else { + await NamidaLinkUtils.openLink(url); + } } - } else { - await NamidaLinkUtils.openLink(url); - } - } - }, - ), - ); + }, + ), + ); - YoutubeController.inst.downloadedFilesMap; // for refreshing. - final downloadedFileExists = YoutubeController.inst.doesIDHasFileDownloaded(currentId) != null; + YoutubeController.inst.downloadedFilesMap; // for refreshing. + final downloadedFileExists = YoutubeController.inst.doesIDHasFileDownloaded(currentId) != null; - final defaultIconColor = context.defaultIconColor(CurrentColor.inst.miniplayerColor); + final defaultIconColor = context.defaultIconColor(CurrentColor.inst.miniplayerColor); - // ==== MiniPlayer Body, contains title, description, comments, ..etc. ==== - final miniplayerBody = Stack( - alignment: Alignment.bottomCenter, - children: [ - // opacity: (percentage * 4 - 3).withMinimum(0), - Listener( - key: Key("${currentId}_body_listener"), - onPointerMove: (event) { - if (event.delta.dy > 0) { - if (YoutubeController.inst.scrollController.hasClients) { - if (YoutubeController.inst.scrollController.position.pixels <= 0) { - _updateCanScrollQueue(false); + // ==== MiniPlayer Body, contains title, description, comments, ..etc. ==== + final miniplayerBody = Stack( + alignment: Alignment.bottomCenter, + children: [ + // opacity: (percentage * 4 - 3).withMinimum(0), + Listener( + key: Key("${currentId}_body_listener"), + onPointerMove: (event) { + if (event.delta.dy > 0) { + if (YoutubeController.inst.scrollController.hasClients) { + if (YoutubeController.inst.scrollController.position.pixels <= 0) { + _updateCanScrollQueue(false); + } + } + } else { + if (_mpState == null || _mpState?.controller.value == 1) _updateCanScrollQueue(true); } - } - } else { - if (_mpState == null || _mpState?.controller.value == 1) _updateCanScrollQueue(true); - } - }, - onPointerDown: (_) { - YoutubeController.inst.cancelDimTimer(); - _updateCanScrollQueue(true); - }, - onPointerUp: (_) { - YoutubeController.inst.startDimTimer(); - _updateCanScrollQueue(true); - }, - child: Navigator( - key: NamidaNavigator.inst.ytMiniplayerCommentsPageKey, - requestFocus: false, - onPopPage: (route, result) => false, - restorationScopeId: currentId, - pages: [ - MaterialPage( - maintainState: true, - child: IgnorePointer( - ignoring: !_canScrollQueue, - child: LazyLoadListView( - key: Key("${currentId}_body_lazy_load_list"), - onReachingEnd: () async { - if (settings.ytTopComments.value) return; - await YoutubeController.inst.updateCurrentComments(currentId, fetchNextOnly: true); - }, - extend: 400, - scrollController: YoutubeController.inst.scrollController, - listview: (controller) => Stack( - key: Key("${currentId}_body_stack"), - children: [ - CustomScrollView( - // key: PageStorageKey(currentId), // duplicate errors - physics: _canScrollQueue ? const ClampingScrollPhysicsModified() : const NeverScrollableScrollPhysics(), - controller: controller, - slivers: [ - // --START-- title & subtitle - SliverToBoxAdapter( - key: Key("${currentId}_title"), - child: ShimmerWrapper( - shimmerDurationMS: 550, - shimmerDelayMS: 250, - shimmerEnabled: videoInfo == null, - child: ExpansionTile( - // key: Key(currentId), - initiallyExpanded: false, - maintainState: false, - expandedAlignment: Alignment.centerLeft, - expandedCrossAxisAlignment: CrossAxisAlignment.start, - tilePadding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 14.0), - textColor: Color.alphaBlend(CurrentColor.inst.miniplayerColor.withAlpha(40), context.theme.colorScheme.onBackground), - collapsedTextColor: context.theme.colorScheme.onBackground, - iconColor: Color.alphaBlend(CurrentColor.inst.miniplayerColor.withAlpha(40), context.theme.colorScheme.onBackground), - collapsedIconColor: context.theme.colorScheme.onBackground, - childrenPadding: const EdgeInsets.all(18.0), - onExpansionChanged: (value) => YoutubeController.inst.isTitleExpanded.value = value, - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Obx( - () { - final videoListens = YoutubeHistoryController.inst.topTracksMapListens[currentId] ?? []; - if (videoListens.isEmpty) return const SizedBox(); - return NamidaInkWell( - borderRadius: 6.0, - bgColor: CurrentColor.inst.miniplayerColor.withOpacity(0.7), - onTap: () { - showVideoListensDialog(currentId); - }, - padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0), - child: Text( - videoListens.length.formatDecimal(), - style: context.textTheme.displaySmall?.copyWith( - color: Colors.white.withOpacity(0.6), - ), - ), - ); - }, - ), - const SizedBox(width: 8.0), - NamidaPopupWrapper( - onPop: () { - _numberOfRepeats.value = 1; - }, - childrenDefault: () { - final videoId = currentId; - final items = YTUtils.getVideoCardMenuItems( - videoId: videoId, - url: videoInfo?.url, - channelUrl: channelIDOrURL, - playlistID: null, - idsNamesLookup: {videoId: videoInfo?.name}, - ); - if (Player.inst.nowPlayingVideoID != null && videoId == Player.inst.getCurrentVideoId) { - final repeatForWidget = NamidaPopupItem( - icon: Broken.cd, - title: '', - titleBuilder: (style) => Obx( - () => Text( - lang.REPEAT_FOR_N_TIMES.replaceFirst('_NUM_', _numberOfRepeats.value.toString()), - style: style, + }, + onPointerDown: (_) { + YoutubeController.inst.cancelDimTimer(); + _updateCanScrollQueue(true); + }, + onPointerUp: (_) { + YoutubeController.inst.startDimTimer(); + _updateCanScrollQueue(true); + }, + child: Navigator( + key: NamidaNavigator.inst.ytMiniplayerCommentsPageKey, + requestFocus: false, + onPopPage: (route, result) => false, + restorationScopeId: currentId, + pages: [ + MaterialPage( + maintainState: true, + child: IgnorePointer( + ignoring: !_canScrollQueue, + child: LazyLoadListView( + key: Key("${currentId}_body_lazy_load_list"), + onReachingEnd: () async { + if (settings.ytTopComments.value) return; + await YoutubeController.inst.updateCurrentComments(currentId, fetchNextOnly: true); + }, + extend: 400, + scrollController: YoutubeController.inst.scrollController, + listview: (controller) => Stack( + key: Key("${currentId}_body_stack"), + children: [ + CustomScrollView( + // key: PageStorageKey(currentId), // duplicate errors + physics: _canScrollQueue ? const ClampingScrollPhysicsModified() : const NeverScrollableScrollPhysics(), + controller: controller, + slivers: [ + // --START-- title & subtitle + SliverToBoxAdapter( + key: Key("${currentId}_title"), + child: ShimmerWrapper( + shimmerDurationMS: 550, + shimmerDelayMS: 250, + shimmerEnabled: videoInfo == null, + child: ExpansionTile( + // key: Key(currentId), + initiallyExpanded: false, + maintainState: false, + expandedAlignment: Alignment.centerLeft, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + tilePadding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 14.0), + textColor: Color.alphaBlend(CurrentColor.inst.miniplayerColor.withAlpha(40), context.theme.colorScheme.onSurface), + collapsedTextColor: context.theme.colorScheme.onSurface, + iconColor: Color.alphaBlend(CurrentColor.inst.miniplayerColor.withAlpha(40), context.theme.colorScheme.onSurface), + collapsedIconColor: context.theme.colorScheme.onSurface, + childrenPadding: const EdgeInsets.all(18.0), + onExpansionChanged: (value) => YoutubeController.inst.isTitleExpanded.value = value, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Obx( + () { + final videoListens = YoutubeHistoryController.inst.topTracksMapListens[currentId] ?? []; + if (videoListens.isEmpty) return const SizedBox(); + return NamidaInkWell( + borderRadius: 6.0, + bgColor: CurrentColor.inst.miniplayerColor.withOpacity(0.7), + onTap: () { + showVideoListensDialog(currentId); + }, + padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0), + child: Text( + videoListens.length.formatDecimal(), + style: context.textTheme.displaySmall?.copyWith( + color: Colors.white.withOpacity(0.6), + ), ), - ), - onTap: () { - settings.player.save(repeatMode: RepeatMode.forNtimes); - Player.inst.updateNumberOfRepeats(_numberOfRepeats.value); - }, - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - NamidaIconButton( - icon: Broken.minus_cirlce, - onPressed: () => _numberOfRepeats.value = (_numberOfRepeats.value - 1).clamp(1, 20), - iconSize: 20.0, + ); + }, + ), + const SizedBox(width: 8.0), + NamidaPopupWrapper( + onPop: () { + _numberOfRepeats.value = 1; + }, + childrenDefault: () { + final videoId = currentId; + final items = YTUtils.getVideoCardMenuItems( + videoId: videoId, + url: videoInfo?.url, + channelUrl: channelIDOrURL, + playlistID: null, + idsNamesLookup: {videoId: videoInfo?.name}, + ); + if (Player.inst.currentVideo != null && videoId == Player.inst.getCurrentVideoId) { + final repeatForWidget = NamidaPopupItem( + icon: Broken.cd, + title: '', + titleBuilder: (style) => Obx( + () => Text( + lang.REPEAT_FOR_N_TIMES.replaceFirst('_NUM_', _numberOfRepeats.valueR.toString()), + style: style, + ), ), - NamidaIconButton( - icon: Broken.add_circle, - onPressed: () => _numberOfRepeats.value = (_numberOfRepeats.value + 1).clamp(1, 20), - iconSize: 20.0, + onTap: () { + settings.player.save(repeatMode: RepeatMode.forNtimes); + Player.inst.updateNumberOfRepeats(_numberOfRepeats.value); + }, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + NamidaIconButton( + icon: Broken.minus_cirlce, + onPressed: () => _numberOfRepeats.value = (_numberOfRepeats.value - 1).clamp(1, 20), + iconSize: 20.0, + ), + NamidaIconButton( + icon: Broken.add_circle, + onPressed: () => _numberOfRepeats.value = (_numberOfRepeats.value + 1).clamp(1, 20), + iconSize: 20.0, + ), + ], ), - ], + ); + items.add(repeatForWidget); + } + items.add( + NamidaPopupItem( + icon: Broken.trash, + title: lang.CLEAR, + onTap: () { + YTUtils().showVideoClearDialog(context, videoId, CurrentColor.inst.miniplayerColor); + }, + ), + ); + return items; + }, + child: const Icon( + Broken.arrow_down_2, + size: 20.0, + ), + ), + ], + ), + title: ObxO( + rx: YoutubeController.inst.isTitleExpanded, + builder: (isTitleExpanded) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NamidaDummyContainer( + width: context.width * 0.8, + height: 24.0, + borderRadius: 6.0, + shimmerEnabled: videoInfo == null, + child: Text( + videoInfo?.name ?? '', + maxLines: isTitleExpanded ? 6 : 2, + overflow: TextOverflow.ellipsis, + style: context.textTheme.displayLarge, ), - ); - items.add(repeatForWidget); - } - items.add( - NamidaPopupItem( - icon: Broken.trash, - title: lang.CLEAR, - onTap: () { - YTUtils().showVideoClearDialog(context, videoId, CurrentColor.inst.miniplayerColor); - }, ), - ); - return items; - }, - child: const Icon( - Broken.arrow_down_2, - size: 20.0, + const SizedBox(height: 4.0), + NamidaDummyContainer( + width: context.width * 0.7, + height: 12.0, + shimmerEnabled: videoInfo == null, + child: () { + final expandedDate = isTitleExpanded ? uploadDate : null; + final collapsedDate = isTitleExpanded ? null : uploadDateAgo; + return Text( + [ + if (videoViewCount != null) + "${videoViewCount.formatDecimalShort(isTitleExpanded)} ${videoViewCount == 0 ? lang.VIEW : lang.VIEWS}", + if (expandedDate != null) expandedDate, + if (collapsedDate != null) collapsedDate, + ].join(' • '), + style: context.textTheme.displaySmall?.copyWith(fontWeight: FontWeight.w500), + ); + }(), + ), + ], ), ), - ], - ), - title: Obx( - () { - final isTitleExpanded = YoutubeController.inst.isTitleExpanded.value; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - NamidaDummyContainer( - width: context.width * 0.8, - height: 24.0, - borderRadius: 6.0, - shimmerEnabled: videoInfo == null, - child: Text( - videoInfo?.name ?? '', - maxLines: isTitleExpanded ? 6 : 2, - overflow: TextOverflow.ellipsis, - style: context.textTheme.displayLarge, - ), - ), - const SizedBox(height: 4.0), - NamidaDummyContainer( - width: context.width * 0.7, - height: 12.0, - shimmerEnabled: videoInfo == null, - child: () { - final expandedDate = isTitleExpanded ? uploadDate : null; - final collapsedDate = isTitleExpanded ? null : uploadDateAgo; - return Text( - [ - if (videoViewCount != null) - "${videoViewCount.formatDecimalShort(isTitleExpanded)} ${videoViewCount == 0 ? lang.VIEW : lang.VIEWS}", - if (expandedDate != null) expandedDate, - if (collapsedDate != null) collapsedDate, - ].join(' • '), - style: context.textTheme.displaySmall?.copyWith(fontWeight: FontWeight.w500), - ); - }(), - ), - ], - ); - }, + children: [ + if (descriptionWidget != null) descriptionWidget, + ], + ), ), - children: [ - if (descriptionWidget != null) descriptionWidget, - ], ), - ), - ), - // --END-- title & subtitle + // --END-- title & subtitle - // --START-- buttons - SliverToBoxAdapter( - key: Key("${currentId}_buttons"), - child: ShimmerWrapper( - shimmerDurationMS: 550, - shimmerDelayMS: 250, - shimmerEnabled: videoInfo == null, - child: SizedBox( - width: context.width, - child: Wrap( - alignment: WrapAlignment.spaceEvenly, - children: [ - const SizedBox(width: 4.0), - Obx( - () { - final isTitleExpanded = YoutubeController.inst.isTitleExpanded.value; - return SmallYTActionButton( - title: videoInfo == null - ? null - : videoLikeCount < 1 - ? lang.LIKE - : videoLikeCount.formatDecimalShort(isTitleExpanded), - icon: Broken.like_1, - smallIconWidget: FittedBox( - child: NamidaRawLikeButton( - likedIcon: Broken.like_filled, - normalIcon: Broken.like_1, - disabledColor: context.theme.iconTheme.color, - isLiked: isUserLiked, - onTap: (isLiked) async { - YoutubePlaylistController.inst.favouriteButtonOnPressed(currentId); - }, + // --START-- buttons + SliverToBoxAdapter( + key: Key("${currentId}_buttons"), + child: ShimmerWrapper( + shimmerDurationMS: 550, + shimmerDelayMS: 250, + shimmerEnabled: videoInfo == null, + child: SizedBox( + width: context.width, + child: Wrap( + alignment: WrapAlignment.spaceEvenly, + children: [ + const SizedBox(width: 4.0), + ObxO( + rx: YoutubeController.inst.isTitleExpanded, + builder: (isTitleExpanded) => SmallYTActionButton( + title: videoInfo == null + ? null + : videoLikeCount < 1 + ? lang.LIKE + : videoLikeCount.formatDecimalShort(isTitleExpanded), + icon: Broken.like_1, + smallIconWidget: FittedBox( + child: NamidaRawLikeButton( + likedIcon: Broken.like_filled, + normalIcon: Broken.like_1, + disabledColor: context.theme.iconTheme.color, + isLiked: isUserLiked, + onTap: (isLiked) async { + YoutubePlaylistController.inst.favouriteButtonOnPressed(currentId); + }, + ), ), ), - ); - }, - ), - const SizedBox(width: 4.0), - Obx( - () { - final isTitleExpanded = YoutubeController.inst.isTitleExpanded.value; - return SmallYTActionButton( - title: (videoDislikeCount ?? 0) < 1 ? lang.DISLIKE : videoDislikeCount?.formatDecimalShort(isTitleExpanded) ?? '?', - icon: Broken.dislike, - onPressed: () {}, - ); - }, - ), - const SizedBox(width: 4.0), - SmallYTActionButton( - title: lang.SHARE, - icon: Broken.share, - onPressed: () { - final url = videoInfo?.url; - if (url != null) Share.share(url); - }, - ), - const SizedBox(width: 4.0), - SmallYTActionButton( - title: lang.REFRESH, - icon: Broken.refresh, - onPressed: () async => await YoutubeController.inst.updateVideoDetails(currentId, forceRequest: true), - ), - const SizedBox(width: 4.0), - Obx( - () { - final audioProgress = YoutubeController.inst.downloadsAudioProgressMap[currentId]?.values.firstOrNull; - final audioPercText = audioProgress?.percentageText(prefix: lang.AUDIO); - final videoProgress = YoutubeController.inst.downloadsVideoProgressMap[currentId]?.values.firstOrNull; - final videoPercText = videoProgress?.percentageText(prefix: lang.VIDEO); + ), + const SizedBox(width: 4.0), + ObxO( + rx: YoutubeController.inst.isTitleExpanded, + builder: (isTitleExpanded) => SmallYTActionButton( + title: (videoDislikeCount ?? 0) < 1 ? lang.DISLIKE : videoDislikeCount?.formatDecimalShort(isTitleExpanded) ?? '?', + icon: Broken.dislike, + onPressed: () {}, + ), + ), + const SizedBox(width: 4.0), + SmallYTActionButton( + title: lang.SHARE, + icon: Broken.share, + onPressed: () { + final url = videoInfo?.url; + if (url != null) Share.share(url); + }, + ), + const SizedBox(width: 4.0), + SmallYTActionButton( + title: lang.REFRESH, + icon: Broken.refresh, + onPressed: () async => await YoutubeController.inst.updateVideoDetails(currentId, forceRequest: true), + ), + const SizedBox(width: 4.0), + Obx( + () { + final audioProgress = YoutubeController.inst.downloadsAudioProgressMap[currentId]?.values.firstOrNull; + final audioPercText = audioProgress?.percentageText(prefix: lang.AUDIO); + final videoProgress = YoutubeController.inst.downloadsVideoProgressMap[currentId]?.values.firstOrNull; + final videoPercText = videoProgress?.percentageText(prefix: lang.VIDEO); - final isDownloading = YoutubeController.inst.isDownloading[currentId]?.values.any((element) => element) == true; + final isDownloading = YoutubeController.inst.isDownloading[currentId]?.values.any((element) => element) == true; - final wasDownloading = videoProgress != null || audioProgress != null; - final icon = (wasDownloading && !isDownloading) - ? Broken.play_circle - : wasDownloading - ? Broken.pause_circle - : downloadedFileExists - ? Broken.tick_circle - : Broken.import; - return SmallYTActionButton( - titleWidget: videoPercText == null && audioPercText == null && isDownloading ? const LoadingIndicator() : null, - title: videoPercText ?? audioPercText ?? lang.DOWNLOAD, - icon: icon, - onLongPress: () async => await showDownloadVideoBottomSheet(videoId: currentId), - onPressed: () async { - if (isDownloading) { - YoutubeController.inst.pauseDownloadTask( - itemsConfig: [], - videosIds: [currentId], - groupName: '', - ); - } else if (wasDownloading) { - YoutubeController.inst.resumeDownloadTaskForIDs( - videosIds: [currentId], - groupName: '', - ); - } else { - await showDownloadVideoBottomSheet(videoId: currentId); - } + final wasDownloading = videoProgress != null || audioProgress != null; + final icon = (wasDownloading && !isDownloading) + ? Broken.play_circle + : wasDownloading + ? Broken.pause_circle + : downloadedFileExists + ? Broken.tick_circle + : Broken.import; + return SmallYTActionButton( + titleWidget: videoPercText == null && audioPercText == null && isDownloading ? const LoadingIndicator() : null, + title: videoPercText ?? audioPercText ?? lang.DOWNLOAD, + icon: icon, + onLongPress: () async => await showDownloadVideoBottomSheet(videoId: currentId), + onPressed: () async { + if (isDownloading) { + YoutubeController.inst.pauseDownloadTask( + itemsConfig: [], + videosIds: [currentId], + groupName: '', + ); + } else if (wasDownloading) { + YoutubeController.inst.resumeDownloadTaskForIDs( + videosIds: [currentId], + groupName: '', + ); + } else { + await showDownloadVideoBottomSheet(videoId: currentId); + } + }, + ); }, - ); - }, - ), - const SizedBox(width: 4.0), - SmallYTActionButton( - title: lang.SAVE, - icon: Broken.music_playlist, - onPressed: () => showAddToPlaylistSheet( - ids: [currentId], - idsNamesLookup: { - currentId: videoInfo?.name ?? '', - }, - ), + ), + const SizedBox(width: 4.0), + SmallYTActionButton( + title: lang.SAVE, + icon: Broken.music_playlist, + onPressed: () => showAddToPlaylistSheet( + ids: [currentId], + idsNamesLookup: { + currentId: videoInfo?.name ?? '', + }, + ), + ), + const SizedBox(width: 4.0), + ], ), - const SizedBox(width: 4.0), - ], + ), ), ), - ), - ), - const SliverPadding(padding: EdgeInsets.only(top: 24.0)), - // --END- buttons + const SliverPadding(padding: EdgeInsets.only(top: 24.0)), + // --END- buttons - // --START- channel - SliverToBoxAdapter( - key: Key("${currentId}_channel"), - child: ShimmerWrapper( - shimmerDurationMS: 550, - shimmerDelayMS: 250, - shimmerEnabled: channelName == null || channelThumbnail == null || channelSubs == null, - child: Material( - type: MaterialType.transparency, - child: InkWell( - onTap: () { - final channel = videoChannel ?? Player.inst.currentChannelInfo; - final chid = channel?.id; - if (chid != null) NamidaNavigator.inst.navigateTo(YTChannelSubpage(channelID: chid, channel: channel)); - }, - child: Row( - children: [ - const SizedBox(width: 18.0), - NamidaDummyContainer( - width: 42.0, - height: 42.0, - borderRadius: 100.0, - shimmerEnabled: channelThumbnail == null && (channelIDOrURL == null || channelIDOrURL == ''), - child: YoutubeThumbnail( - key: Key("${channelThumbnail}_$channelIDOrURL"), - isImportantInCache: true, - channelUrl: channelThumbnail ?? '', - channelIDForHQImage: channelIDOrURL ?? '', - width: 42.0, - height: 42.0, - isCircle: true, - ), - ), - const SizedBox(width: 8.0), - Expanded( - // key: Key(currentId), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FittedBox( - child: Row( - children: [ - NamidaDummyContainer( - width: 114.0, - height: 12.0, + // --START- channel + SliverToBoxAdapter( + key: Key("${currentId}_channel"), + child: ShimmerWrapper( + shimmerDurationMS: 550, + shimmerDelayMS: 250, + shimmerEnabled: channelName == null || channelThumbnail == null || channelSubs == null, + child: Material( + type: MaterialType.transparency, + child: InkWell( + onTap: () { + final channel = videoChannel ?? Player.inst.currentChannelInfo.value; + final chid = channel?.id; + if (chid != null) NamidaNavigator.inst.navigateTo(YTChannelSubpage(channelID: chid, channel: channel)); + }, + child: Row( + children: [ + const SizedBox(width: 18.0), + NamidaDummyContainer( + width: 42.0, + height: 42.0, + borderRadius: 100.0, + shimmerEnabled: channelThumbnail == null && (channelIDOrURL == null || channelIDOrURL == ''), + child: YoutubeThumbnail( + key: Key("${channelThumbnail}_$channelIDOrURL"), + isImportantInCache: true, + channelUrl: channelThumbnail ?? '', + channelIDForHQImage: channelIDOrURL ?? '', + width: 42.0, + height: 42.0, + isCircle: true, + ), + ), + const SizedBox(width: 8.0), + Expanded( + // key: Key(currentId), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FittedBox( + child: Row( + children: [ + NamidaDummyContainer( + width: 114.0, + height: 12.0, + borderRadius: 4.0, + shimmerEnabled: channelName == null, + child: Text( + channelName ?? '', + style: context.textTheme.displayMedium?.copyWith( + fontSize: 13.5, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ), + ), + if (channelIsVerified) ...[ + const SizedBox(width: 4.0), + const Icon( + Broken.shield_tick, + size: 12.0, + ), + ] + ], + ), + ), + const SizedBox(height: 2.0), + FittedBox( + child: NamidaDummyContainer( + width: 92.0, + height: 10.0, borderRadius: 4.0, - shimmerEnabled: channelName == null, - child: Text( - channelName ?? '', - style: context.textTheme.displayMedium?.copyWith( - fontSize: 13.5.multipliedFontScale, + shimmerEnabled: channelSubs == null, + child: ObxO( + rx: YoutubeController.inst.isTitleExpanded, + builder: (isTitleExpanded) => Text( + channelSubs == null + ? '? ${lang.SUBSCRIBERS}' + : [ + channelSubs.formatDecimalShort(isTitleExpanded), + channelSubs < 2 ? lang.SUBSCRIBER : lang.SUBSCRIBERS, + ].join(' '), + style: context.textTheme.displaySmall?.copyWith( + fontSize: 12.0, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.start, ), ), - if (channelIsVerified) ...[ - const SizedBox(width: 4.0), - const Icon( - Broken.shield_tick, - size: 12.0, - ), - ] - ], - ), - ), - const SizedBox(height: 2.0), - FittedBox( - child: NamidaDummyContainer( - width: 92.0, - height: 10.0, - borderRadius: 4.0, - shimmerEnabled: channelSubs == null, - child: Obx( - () { - final isTitleExpanded = YoutubeController.inst.isTitleExpanded.value; - return Text( - channelSubs == null - ? '? ${lang.SUBSCRIBERS}' - : [ - channelSubs.formatDecimalShort(isTitleExpanded), - channelSubs < 2 ? lang.SUBSCRIBER : lang.SUBSCRIBERS, - ].join(' '), - style: context.textTheme.displaySmall?.copyWith( - fontSize: 12.0.multipliedFontScale, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ); - }, ), - ), + ], ), - ], - ), + ), + const SizedBox(width: 12.0), + YTSubscribeButton(channelIDOrURL: channelIDOrURL), + const SizedBox(width: 20.0), + ], ), - const SizedBox(width: 12.0), - YTSubscribeButton(channelIDOrURL: channelIDOrURL), - const SizedBox(width: 20.0), - ], + ), ), ), ), - ), - ), - const SliverPadding(padding: EdgeInsets.only(top: 4.0)), - // --END-- channel + const SliverPadding(padding: EdgeInsets.only(top: 4.0)), + // --END-- channel - // --SRART-- top comments - const SliverPadding(padding: EdgeInsets.only(top: 4.0)), - Obx( - () { - if (!settings.ytTopComments.value) return const SliverToBoxAdapter(child: SizedBox()); - final totalCommentsCount = YoutubeController.inst.currentTotalCommentsCount.value; - final comments = YoutubeController.inst.currentComments; - return SliverToBoxAdapter( - child: comments.isEmpty - ? const SizedBox() - : NamidaInkWell( - key: Key("${currentId}_top_comments_highlight"), - bgColor: Color.alphaBlend(context.theme.scaffoldBackgroundColor.withOpacity(0.4), context.theme.cardColor), - margin: const EdgeInsets.symmetric(horizontal: 18.0), - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - onTap: () { - NamidaNavigator.inst.isInYTCommentsSubpage = true; - NamidaNavigator.inst.ytMiniplayerCommentsPageKey?.currentState?.push( - GetPageRoute( - page: () => const YTMiniplayerCommentsSubpage(), - transition: Transition.cupertino, - ), - ); - }, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, + // --SRART-- top comments + const SliverPadding(padding: EdgeInsets.only(top: 4.0)), + Obx( + () { + if (!settings.ytTopComments.valueR) return const SliverToBoxAdapter(child: SizedBox()); + final totalCommentsCount = YoutubeController.inst.currentTotalCommentsCount.valueR; + final comments = YoutubeController.inst.currentComments.valueR; + return SliverToBoxAdapter( + child: comments.isEmpty + ? const SizedBox() + : NamidaInkWell( + key: Key("${currentId}_top_comments_highlight"), + bgColor: Color.alphaBlend(context.theme.scaffoldBackgroundColor.withOpacity(0.4), context.theme.cardColor), + margin: const EdgeInsets.symmetric(horizontal: 18.0), + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + onTap: () { + NamidaNavigator.inst.isInYTCommentsSubpage = true; + NamidaNavigator.inst.ytMiniplayerCommentsPageKey.currentState?.pushPage( + const YTMiniplayerCommentsSubpage(), + maintainState: false, + ); + }, + child: Column( children: [ - const Icon( - Broken.document, - size: 16.0, - ), - const SizedBox(width: 8.0), - Text( - [ - lang.COMMENTS, - if (totalCommentsCount != null) totalCommentsCount.formatDecimalShort(), - ].join(' • '), - style: context.textTheme.displaySmall, - textAlign: TextAlign.start, + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Icon( + Broken.document, + size: 16.0, + ), + const SizedBox(width: 8.0), + Text( + [ + lang.COMMENTS, + if (totalCommentsCount != null) totalCommentsCount.formatDecimalShort(), + ].join(' • '), + style: context.textTheme.displaySmall, + textAlign: TextAlign.start, + ), + const Spacer(), + NamidaIconButton( + horizontalPadding: 0.0, + tooltip: YoutubeController.inst.isCurrentCommentsFromCache ? lang.CACHE : null, + icon: Broken.refresh, + iconSize: 22.0, + onPressed: () async => await YoutubeController.inst.updateCurrentComments( + currentId, + forceRequest: ConnectivityController.inst.hasConnection, + ), + child: YoutubeController.inst.isCurrentCommentsFromCache + ? StackedIcon( + baseIcon: Broken.refresh, + secondaryIcon: Broken.global, + iconSize: 20.0, + secondaryIconSize: 12.0, + baseIconColor: defaultIconColor, + secondaryIconColor: defaultIconColor, + ) + : Icon( + Broken.refresh, + color: defaultIconColor, + size: 20.0, + ), + ) + ], ), - const Spacer(), - NamidaIconButton( - horizontalPadding: 0.0, - tooltip: YoutubeController.inst.isCurrentCommentsFromCache ? lang.CACHE : null, - icon: Broken.refresh, - iconSize: 22.0, - onPressed: () async => await YoutubeController.inst.updateCurrentComments( - currentId, - forceRequest: ConnectivityController.inst.hasConnection, - ), - child: YoutubeController.inst.isCurrentCommentsFromCache - ? StackedIcon( - baseIcon: Broken.refresh, - secondaryIcon: Broken.global, - iconSize: 20.0, - secondaryIconSize: 12.0, - baseIconColor: defaultIconColor, - secondaryIconColor: defaultIconColor, - ) - : Icon( - Broken.refresh, - color: defaultIconColor, - size: 20.0, - ), + const NamidaContainerDivider(margin: EdgeInsets.symmetric(vertical: 4.0)), + ShimmerWrapper( + shimmerEnabled: comments.isNotEmpty && comments.first == null, + child: YTCommentCardCompact(comment: comments.firstOrNull), ) ], ), - const NamidaContainerDivider(margin: EdgeInsets.symmetric(vertical: 4.0)), - ShimmerWrapper( - shimmerEnabled: comments.isNotEmpty && comments.first == null, - child: YTCommentCardCompact(comment: comments.firstOrNull), - ) - ], + ), + ); + }, + ), + const SliverPadding(padding: EdgeInsets.only(top: 8.0)), + + Obx( + () { + final feed = YoutubeController.inst.currentRelatedVideos.valueR; + if (feed.isNotEmpty && feed.first == null) { + return SliverToBoxAdapter( + key: Key("${currentId}_feed_shimmer"), + child: ShimmerWrapper( + transparent: false, + shimmerEnabled: true, + child: ListView.builder( + padding: EdgeInsets.zero, + key: Key("${currentId}_feedlist_shimmer"), + physics: const NeverScrollableScrollPhysics(), + itemCount: feed.length, + shrinkWrap: true, + itemBuilder: (context, index) { + const item = null; + return YoutubeVideoCard( + key: Key("${item == null}_${context.hashCode}"), + thumbnailHeight: relatedThumbnailHeight, + thumbnailWidth: relatedThumbnailWidth, + isImageImportantInCache: false, + video: item, + playlistID: null, + ); + }, ), ), - ); - }, - ), - const SliverPadding(padding: EdgeInsets.only(top: 8.0)), - - Obx( - () { - final feed = YoutubeController.inst.currentRelatedVideos; - if (feed.isNotEmpty && feed.first == null) { - return SliverToBoxAdapter( - key: Key("${currentId}_feed_shimmer"), - child: ShimmerWrapper( - transparent: false, - shimmerEnabled: true, - child: ListView.builder( - padding: EdgeInsets.zero, - key: Key("${currentId}_feedlist_shimmer"), - physics: const NeverScrollableScrollPhysics(), - itemCount: feed.length, - shrinkWrap: true, - itemBuilder: (context, index) { - const item = null; + ); + } + return SliverFixedExtentList.builder( + key: Key("${currentId}_feedlist"), + itemExtent: relatedThumbnailItemExtent, + itemCount: feed.length, + itemBuilder: (context, index) { + final item = feed[index]; + if (item is StreamInfoItem || item == null) { return YoutubeVideoCard( - key: Key("${item == null}_${context.hashCode}"), + key: Key("${item == null}_${context.hashCode}_${(item as StreamInfoItem?)?.id}"), thumbnailHeight: relatedThumbnailHeight, thumbnailWidth: relatedThumbnailWidth, isImageImportantInCache: false, video: item, playlistID: null, ); - }, - ), - ), - ); - } - return SliverFixedExtentList.builder( - key: Key("${currentId}_feedlist"), - itemExtent: relatedThumbnailItemExtent, - itemCount: feed.length, - itemBuilder: (context, index) { - final item = feed[index]; - if (item is StreamInfoItem || item == null) { - return YoutubeVideoCard( - key: Key("${item == null}_${context.hashCode}_${(item as StreamInfoItem?)?.id}"), - thumbnailHeight: relatedThumbnailHeight, - thumbnailWidth: relatedThumbnailWidth, - isImageImportantInCache: false, - video: item, - playlistID: null, - ); - } else if (item is YoutubePlaylist) { - return YoutubePlaylistCard( - key: Key("${context.hashCode}_${(item).id}"), - playlist: item, - playOnTap: true, - ); - } else if (item is YoutubeChannel) { - return YoutubeChannelCard( - key: Key("${context.hashCode}_${(item as YoutubeChannelCard).channel?.id}"), - channel: item, - ); - } - return const SizedBox(); + } else if (item is YoutubePlaylist) { + return YoutubePlaylistCard( + key: Key("${context.hashCode}_${(item).id}"), + playlist: item, + playOnTap: true, + ); + } else if (item is YoutubeChannel) { + return YoutubeChannelCard( + key: Key("${context.hashCode}_${(item as YoutubeChannelCard).channel?.id}"), + channel: item, + ); + } + return const SizedBox(); + }, + ); }, - ); - }, - ), - const SliverPadding(padding: EdgeInsets.only(top: 12.0)), + ), + const SliverPadding(padding: EdgeInsets.only(top: 12.0)), - // --START-- Comments - Obx( - () { - if (settings.ytTopComments.value) return const SliverToBoxAdapter(child: SizedBox()); + // --START-- Comments + Obx( + () { + if (settings.ytTopComments.valueR) return const SliverToBoxAdapter(child: SizedBox()); - final totalCommentsCount = YoutubeController.inst.currentTotalCommentsCount.value; - return SliverToBoxAdapter( - child: Padding( - key: Key("${currentId}_comments_header"), - padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const Icon(Broken.document), - const SizedBox(width: 8.0), - Text( - [ - lang.COMMENTS, - if (totalCommentsCount != null) totalCommentsCount.formatDecimalShort(), - ].join(' • '), - style: context.textTheme.displayLarge, - textAlign: TextAlign.start, + final totalCommentsCount = YoutubeController.inst.currentTotalCommentsCount.valueR; + return SliverToBoxAdapter( + child: Padding( + key: Key("${currentId}_comments_header"), + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Icon(Broken.document), + const SizedBox(width: 8.0), + Text( + [ + lang.COMMENTS, + if (totalCommentsCount != null) totalCommentsCount.formatDecimalShort(), + ].join(' • '), + style: context.textTheme.displayLarge, + textAlign: TextAlign.start, + ), + const Spacer(), + NamidaIconButton( + // key: Key(currentId), + tooltip: YoutubeController.inst.isCurrentCommentsFromCache ? lang.CACHE : null, + icon: Broken.refresh, + iconSize: 22.0, + onPressed: () async => await YoutubeController.inst.updateCurrentComments( + currentId, + forceRequest: ConnectivityController.inst.hasConnection, + ), + child: YoutubeController.inst.isCurrentCommentsFromCache + ? const StackedIcon( + baseIcon: Broken.refresh, + secondaryIcon: Broken.global, + ) + : Icon( + Broken.refresh, + color: defaultIconColor, + ), + ) + ], ), - const Spacer(), - NamidaIconButton( - // key: Key(currentId), - tooltip: YoutubeController.inst.isCurrentCommentsFromCache ? lang.CACHE : null, - icon: Broken.refresh, - iconSize: 22.0, - onPressed: () async => await YoutubeController.inst.updateCurrentComments( - currentId, - forceRequest: ConnectivityController.inst.hasConnection, - ), - child: YoutubeController.inst.isCurrentCommentsFromCache - ? const StackedIcon( - baseIcon: Broken.refresh, - secondaryIcon: Broken.global, - ) - : Icon( - Broken.refresh, - color: defaultIconColor, - ), - ) - ], - ), - ), - ); - }, - ), - Obx( - () { - if (settings.ytTopComments.value) return const SliverToBoxAdapter(child: SizedBox()); - - final comments = YoutubeController.inst.currentComments; - if (comments.isNotEmpty && comments.first == null) { - return SliverToBoxAdapter( - key: Key("${currentId}_comments_shimmer"), - child: ShimmerWrapper( - transparent: false, - shimmerEnabled: true, - child: ListView.builder( - // key: Key(currentId), - physics: const NeverScrollableScrollPhysics(), - itemCount: comments.length, - shrinkWrap: true, - itemBuilder: (context, index) { - const comment = null; - return YTCommentCard( - key: Key("${comment == null}_${context.hashCode}"), - margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), - comment: comment, - ); - }, ), - ), - ); - } - return SliverList.builder( - key: Key("${currentId}_comments"), - itemCount: comments.length, - itemBuilder: (context, i) { - final comment = comments[i]; - return YTCommentCard( - key: Key("${comment == null}_${context.hashCode}_${comment?.commentId}"), - margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), - comment: comment, ); }, - ); - }, - ), - Obx( - () { - if (settings.ytTopComments.value) return const SliverToBoxAdapter(child: SizedBox()); + ), + Obx( + () { + if (settings.ytTopComments.valueR) return const SliverToBoxAdapter(child: SizedBox()); - final isLoadingComments = YoutubeController.inst.isLoadingComments.value; - return isLoadingComments - ? const SliverToBoxAdapter( - child: Padding( - padding: EdgeInsets.all(12.0), - child: Center( - child: LoadingIndicator(), + final comments = YoutubeController.inst.currentComments.valueR; + if (comments.isNotEmpty && comments.first == null) { + return SliverToBoxAdapter( + key: Key("${currentId}_comments_shimmer"), + child: ShimmerWrapper( + transparent: false, + shimmerEnabled: true, + child: ListView.builder( + // key: Key(currentId), + physics: const NeverScrollableScrollPhysics(), + itemCount: comments.length, + shrinkWrap: true, + itemBuilder: (context, index) { + const comment = null; + return YTCommentCard( + key: Key("${comment == null}_${context.hashCode}"), + margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + comment: comment, + ); + }, ), ), - ) - : const SliverToBoxAdapter(child: SizedBox()); + ); + } + return SliverList.builder( + key: Key("${currentId}_comments"), + itemCount: comments.length, + itemBuilder: (context, i) { + final comment = comments[i]; + return YTCommentCard( + key: Key("${comment == null}_${context.hashCode}_${comment?.commentId}"), + margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + comment: comment, + ); + }, + ); + }, + ), + Obx( + () { + if (settings.ytTopComments.valueR) return const SliverToBoxAdapter(child: SizedBox()); + + final isLoadingComments = YoutubeController.inst.isLoadingComments.valueR; + return isLoadingComments + ? const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.all(12.0), + child: Center( + child: LoadingIndicator(), + ), + ), + ) + : const SliverToBoxAdapter(child: SizedBox()); + }, + ), + + const SliverPadding(padding: EdgeInsets.only(bottom: kYTQueueSheetMinHeight)) + ], + ), + ObxO( + rx: YoutubeController.inst.shouldShowGlowUnderVideo, + builder: (shouldShowGlowUnderVideo) { + const containerHeight = 12.0; + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: shouldShowGlowUnderVideo + ? Stack( + key: const Key('actual_glow'), + children: [ + Container( + height: containerHeight, + color: context.theme.scaffoldBackgroundColor, + ), + Container( + height: containerHeight, + transform: Matrix4.translationValues(0, containerHeight / 2, 0), + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: context.theme.scaffoldBackgroundColor, + spreadRadius: containerHeight * 0.25, + offset: const Offset(0, 0), + blurRadius: 8.0, + ), + ], + ), + ), + ], + ) + : const SizedBox(key: Key('empty_glow')), + ); }, ), - - const SliverPadding(padding: EdgeInsets.only(bottom: kYTQueueSheetMinHeight)) ], ), - Obx( - () { - const containerHeight = 12.0; - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: YoutubeController.inst.shouldShowGlowUnderVideo - ? Stack( - key: const Key('actual_glow'), - children: [ - Container( - height: containerHeight, - color: context.theme.scaffoldBackgroundColor, - ), - Container( - height: containerHeight, - transform: Matrix4.translationValues(0, containerHeight / 2, 0), - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: context.theme.scaffoldBackgroundColor, - spreadRadius: containerHeight * 0.25, - offset: const Offset(0, 0), - blurRadius: 8.0, - ), - ], - ), - ), - ], - ) - : const SizedBox(key: Key('empty_glow')), - ); - }, - ), - ], + ), ), ), - ), + ], ), - ], - ), - ), + ), - YTMiniplayerQueueChip(key: NamidaNavigator.inst.ytQueueSheetKey), + YTMiniplayerQueueChip(key: NamidaNavigator.inst.ytQueueSheetKey), - // -- dimming - Positioned.fill( - key: const Key('dimmie'), - child: IgnorePointer( - child: Obx( - () => AnimatedSwitcher( - duration: const Duration(milliseconds: 600), - reverseDuration: const Duration(milliseconds: 200), - child: YoutubeController.inst.canDimMiniplayer - ? Container( - color: Colors.black.withOpacity(settings.ytMiniplayerDimOpacity.value), - ) - : null, + // -- dimming + Positioned.fill( + key: const Key('dimmie'), + child: IgnorePointer( + child: Obx( + () => AnimatedSwitcher( + duration: const Duration(milliseconds: 600), + reverseDuration: const Duration(milliseconds: 200), + child: YoutubeController.inst.canDimMiniplayer.valueR + ? Container( + color: Colors.black.withOpacity(settings.ytMiniplayerDimOpacity.valueR), + ) + : null, + ), + ), ), ), - ), - ), - // prevent accidental scroll while performing home gesture - AbsorbPointer( - child: SizedBox( - height: 18.0, - width: context.width, - ), - ), - ], - ); - - final titleChild = Column( - key: Key("${currentId}_title_button1_child"), - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - NamidaDummyContainer( - borderRadius: 4.0, - height: 16.0, - shimmerEnabled: videoInfo == null, - width: context.width - 24.0, - child: Text( - miniTitle ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: context.textTheme.displayMedium?.copyWith( - fontWeight: FontWeight.w600, - fontSize: 13.5.multipliedFontScale, - ), - ), - ), - const SizedBox(height: 4.0), - NamidaDummyContainer( - borderRadius: 4.0, - height: 10.0, - shimmerEnabled: videoInfo == null, - width: context.width - 24.0 * 2, - child: Text( - miniSubtitle ?? '', - style: context.textTheme.displaySmall?.copyWith( - fontWeight: FontWeight.w500, - fontSize: 13.0.multipliedFontScale, + // prevent accidental scroll while performing home gesture + AbsorbPointer( + child: SizedBox( + height: 18.0, + width: context.width, + ), ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ); + ], + ); - final playPauseButtonChild = Obx( - () { - final isLoading = Player.inst.shouldShowLoadingIndicator; - return Stack( - alignment: Alignment.center, + final titleChild = Column( + key: Key("${currentId}_title_button1_child"), + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (isLoading) - IgnorePointer( - child: NamidaOpacity( - key: Key("${currentId}_button_loading"), - enabled: true, - opacity: 0.3, - child: ThreeArchedCircle( - key: Key("${currentId}_button_loading_child"), - color: defaultIconColor, - size: 36.0, - ), + NamidaDummyContainer( + borderRadius: 4.0, + height: 16.0, + shimmerEnabled: videoInfo == null, + width: context.width - 24.0, + child: Text( + miniTitle ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: context.textTheme.displayMedium?.copyWith( + fontWeight: FontWeight.w600, + fontSize: 13.5, ), ), - NamidaIconButton( - horizontalPadding: 0.0, - onPressed: () { - Player.inst.togglePlayPause(); - }, - icon: null, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: Player.inst.isPlaying - ? Icon( - Broken.pause, - color: defaultIconColor, - key: const Key('pause'), - ) - : Icon( - Broken.play, - color: defaultIconColor, - key: const Key('play'), - ), + ), + const SizedBox(height: 4.0), + NamidaDummyContainer( + borderRadius: 4.0, + height: 10.0, + shimmerEnabled: videoInfo == null, + width: context.width - 24.0 * 2, + child: Text( + miniSubtitle ?? '', + style: context.textTheme.displaySmall?.copyWith( + fontWeight: FontWeight.w500, + fontSize: 13.0, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), ], ); - }, - ); - final nextButton = NamidaIconButton( - horizontalPadding: 0.0, - icon: Broken.next, - iconColor: defaultIconColor, - onPressed: () { - Player.inst.next(); - }, - ); - final rightDragAbsorberWidget = Align( - alignment: Alignment.centerRight, - child: SizedBox( - height: context.height, - width: (context.width * 0.25).withMaximum(324.0), - child: Listener( - behavior: HitTestBehavior.translucent, - onPointerDown: (event) { - _mpState?.updatePercentageMultiplier(true); - _mpState?.saveDragHeightStart(); - _velocity.addPosition(event.timeStamp, event.position); - }, - onPointerMove: (event) { - if (!_canScrollQueue) { - _mpState?.onVerticalDragUpdate(event.delta.dy); - _velocity.addPosition(event.timeStamp, event.position); - } + final playPauseButtonChild = Obx( + () { + final isLoading = Player.inst.shouldShowLoadingIndicatorR; + return Stack( + alignment: Alignment.center, + children: [ + if (isLoading) + IgnorePointer( + child: NamidaOpacity( + key: Key("${currentId}_button_loading"), + enabled: true, + opacity: 0.3, + child: ThreeArchedCircle( + key: Key("${currentId}_button_loading_child"), + color: defaultIconColor, + size: 36.0, + ), + ), + ), + NamidaIconButton( + horizontalPadding: 0.0, + onPressed: () { + Player.inst.togglePlayPause(); + }, + icon: null, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: Player.inst.isPlayingR + ? Icon( + Broken.pause, + color: defaultIconColor, + key: const Key('pause'), + ) + : Icon( + Broken.play, + color: defaultIconColor, + key: const Key('play'), + ), + ), + ), + ], + ); }, - onPointerUp: (event) { - if (YoutubeController.inst.scrollController.hasClients && YoutubeController.inst.scrollController.position.pixels <= 0) { - _mpState?.onVerticalDragEnd(_velocity.getVelocity().pixelsPerSecond.dy); - } - _mpState?.updatePercentageMultiplier(false); + ); + final nextButton = NamidaIconButton( + horizontalPadding: 0.0, + icon: Broken.next, + iconColor: defaultIconColor, + onPressed: () { + Player.inst.next(); }, - ), - ), - ); - return NamidaYTMiniplayer( - key: MiniPlayerController.inst.ytMiniplayerKey, - duration: const Duration(milliseconds: 1000), - curve: Curves.easeOutExpo, - bottomMargin: 8.0 + (settings.enableBottomNavBar.value ? kBottomNavigationBarHeight : 0.0) - 1.0, // -1 is just a clip ensurer. - minHeight: miniplayerHeight, - maxHeight: context.height, - bgColor: miniplayerBGColor, - displayBottomBGLayer: !settings.enableBottomNavBar.value, - onDismiss: settings.dismissibleMiniplayer.value ? Player.inst.clearQueue : null, - onDismissing: (dismissPercentage) { - Player.inst.setPlayerVolume(dismissPercentage.clamp(0.0, settings.player.volume.value)); - }, - onHeightChange: (percentage) { - MiniPlayerController.inst.animateMiniplayer(percentage); - }, - onAlternativePercentageExecute: () { - VideoController.inst.toggleFullScreenVideoView( - isLocal: false, - setOrientations: false, ); - }, - builder: (double height, double p) { - final percentage = (p * 2.8).clamp(0.0, 1.0); - final percentageFast = (p * 1.5 - 0.5).clamp(0.0, 1.0); - final inversePerc = 1 - percentage; - final reverseOpacity = (inversePerc * 2.8 - 1.8).clamp(0.0, 1.0); - final finalspace1sb = space1sb * inversePerc; - final finalspace3sb = space3sb * inversePerc; - final finalspace4buttons = space4 * inversePerc; - final finalspace5sb = space5sb * inversePerc; - final finalpadding = 4.0 * inversePerc; - final finalbr = (8.0 * inversePerc).multipliedRadius; - final finalthumbnailWidth = (space2ForThumbnail + context.width * percentage).clamp(space2ForThumbnail, context.width - finalspace1sb - finalspace3sb); - final finalthumbnailHeight = finalthumbnailWidth * 9 / 16; - return Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, + final rightDragAbsorberWidget = Align( + alignment: Alignment.centerRight, + child: SizedBox( + height: context.height, + width: (context.width * 0.25).withMaximum(324.0), + child: Listener( + behavior: HitTestBehavior.translucent, + onPointerDown: (event) { + _mpState?.updatePercentageMultiplier(true); + _mpState?.setDragExternally(true); + _mpState?.saveDragHeightStart(); + _velocity.addPosition(event.timeStamp, event.position); + }, + onPointerMove: (event) { + if (!_canScrollQueue) { + _mpState?.onVerticalDragUpdate(event.delta.dy); + _velocity.addPosition(event.timeStamp, event.position); + } + }, + onPointerUp: (event) { + if (YoutubeController.inst.scrollController.hasClients && YoutubeController.inst.scrollController.position.pixels <= 0) { + _mpState?.onVerticalDragEnd(_velocity.getVelocity().pixelsPerSecond.dy); + } + _mpState?.updatePercentageMultiplier(false); + // thats because the internal GestureDetector executes drag end after Listener's onPointerUp + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _mpState?.setDragExternally(false); + }); + }, + ), + ), + ); + return NamidaYTMiniplayer( + key: MiniPlayerController.inst.ytMiniplayerKey, + duration: const Duration(milliseconds: 1000), + curve: Curves.easeOutExpo, + bottomMargin: 8.0 + (settings.enableBottomNavBar.valueR ? kBottomNavigationBarHeight : 0.0) - 1.0, // -1 is just a clip ensurer. + minHeight: miniplayerHeight, + maxHeight: context.height, + bgColor: miniplayerBGColor, + displayBottomBGLayer: !settings.enableBottomNavBar.valueR, + onDismiss: settings.dismissibleMiniplayer.valueR ? Player.inst.clearQueue : null, + onDismissing: (dismissPercentage) { + Player.inst.setPlayerVolume(dismissPercentage.clamp(0.0, settings.player.volume.value)); + }, + onHeightChange: (percentage) { + MiniPlayerController.inst.animateMiniplayer(percentage); + }, + onAlternativePercentageExecute: () { + VideoController.inst.toggleFullScreenVideoView( + isLocal: false, + setOrientations: false, + ); + }, + builder: (double height, double p) { + final percentage = (p * 2.8).clamp(0.0, 1.0); + final percentageFast = (p * 1.5 - 0.5).clamp(0.0, 1.0); + final inversePerc = 1 - percentage; + final reverseOpacity = (inversePerc * 2.8 - 1.8).clamp(0.0, 1.0); + final finalspace1sb = space1sb * inversePerc; + final finalspace3sb = space3sb * inversePerc; + final finalspace4buttons = space4 * inversePerc; + final finalspace5sb = space5sb * inversePerc; + final finalpadding = 4.0 * inversePerc; + final finalbr = (8.0 * inversePerc).multipliedRadius; + final finalthumbnailWidth = (space2ForThumbnail + context.width * percentage).clamp(space2ForThumbnail, context.width - finalspace1sb - finalspace3sb); + final finalthumbnailHeight = finalthumbnailWidth * 9 / 16; + + return Stack( children: [ - Row( + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(width: finalspace1sb), - Container( - clipBehavior: Clip.antiAlias, - margin: EdgeInsets.symmetric(vertical: finalpadding), - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(finalbr), - ), - width: finalthumbnailWidth, - height: finalthumbnailHeight, - child: NamidaVideoWidget( - isLocal: false, - enableControls: percentage > 0.5, - onMinimizeTap: () => MiniPlayerController.inst.ytMiniplayerKey.currentState?.animateToState(false), - swipeUpToFullscreen: true, - ), - ), - if (reverseOpacity > 0) ...[ - SizedBox(width: finalspace3sb), - SizedBox( - width: (context.width - finalthumbnailWidth - finalspace1sb - finalspace3sb - finalspace4buttons - finalspace5sb).clamp(0, context.width), - child: NamidaOpacity( - key: Key("${currentId}_title_button1"), - enabled: true, - opacity: reverseOpacity, - child: titleChild, - ), - ), - NamidaOpacity( - key: Key("${currentId}_title_button2"), - enabled: true, - opacity: reverseOpacity, - child: SizedBox( - key: Key("${currentId}_title_button2_child"), - width: finalspace4buttons / 2, - height: miniplayerHeight, - child: playPauseButtonChild, - ), - ), - NamidaOpacity( - key: Key("${currentId}_title_button3"), - enabled: true, - opacity: reverseOpacity, - child: SizedBox( - key: Key("${currentId}_title_button3_child"), - width: finalspace4buttons / 2, - height: miniplayerHeight, - child: nextButton, - ), - ), - SizedBox(width: finalspace5sb), - ] - ], - ), - - // ---- if was in comments subpage, and this gets hidden, the route is popped - // ---- same with [isQueueSheetOpen] - if (NamidaNavigator.inst.isInYTCommentsSubpage || NamidaNavigator.inst.isQueueSheetOpen ? true : percentage > 0) - Expanded( - child: Stack( - fit: StackFit.expand, + Row( children: [ - miniplayerBody, - rightDragAbsorberWidget, - IgnorePointer( - child: ColoredBox( - color: miniplayerBGColor.withOpacity(1 - percentageFast), + SizedBox(width: finalspace1sb), + Container( + clipBehavior: Clip.antiAlias, + margin: EdgeInsets.symmetric(vertical: finalpadding), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(finalbr), + ), + width: finalthumbnailWidth, + height: finalthumbnailHeight, + child: NamidaVideoWidget( + isLocal: false, + enableControls: percentage > 0.5, + onMinimizeTap: () => MiniPlayerController.inst.ytMiniplayerKey.currentState?.animateToState(false), + swipeUpToFullscreen: true, ), ), + if (reverseOpacity > 0) ...[ + SizedBox(width: finalspace3sb), + SizedBox( + width: (context.width - finalthumbnailWidth - finalspace1sb - finalspace3sb - finalspace4buttons - finalspace5sb).clamp(0, context.width), + child: NamidaOpacity( + key: Key("${currentId}_title_button1"), + enabled: true, + opacity: reverseOpacity, + child: titleChild, + ), + ), + NamidaOpacity( + key: Key("${currentId}_title_button2"), + enabled: true, + opacity: reverseOpacity, + child: SizedBox( + key: Key("${currentId}_title_button2_child"), + width: finalspace4buttons / 2, + height: miniplayerHeight, + child: playPauseButtonChild, + ), + ), + NamidaOpacity( + key: Key("${currentId}_title_button3"), + enabled: true, + opacity: reverseOpacity, + child: SizedBox( + key: Key("${currentId}_title_button3_child"), + width: finalspace4buttons / 2, + height: miniplayerHeight, + child: nextButton, + ), + ), + SizedBox(width: finalspace5sb), + ] ], ), - ), + + // ---- if was in comments subpage, and this gets hidden, the route is popped + // ---- same with [isQueueSheetOpen] + if (NamidaNavigator.inst.isInYTCommentsSubpage || NamidaNavigator.inst.isQueueSheetOpen ? true : percentage > 0) + Expanded( + child: Stack( + fit: StackFit.expand, + children: [ + miniplayerBody, + rightDragAbsorberWidget, + IgnorePointer( + child: ColoredBox( + color: miniplayerBGColor.withOpacity(1 - percentageFast), + ), + ), + ], + ), + ), + ], + ), + Positioned( + top: finalthumbnailHeight - + (_extraPaddingForYTMiniplayer / 2 * (1 - percentage)) - + (SeekReadyDimensions.barHeight / 2) - + (SeekReadyDimensions.barHeight / 2 * percentage) + + (SeekReadyDimensions.progressBarHeight / 2), + left: 0, + right: 0, + child: seekReadyWidget, + ), ], - ), - Positioned( - top: finalthumbnailHeight - - (_extraPaddingForYTMiniplayer / 2 * (1 - percentage)) - - (SeekReadyDimensions.barHeight / 2) - - (SeekReadyDimensions.barHeight / 2 * percentage) + - (SeekReadyDimensions.progressBarHeight / 2), - left: 0, - right: 0, - child: seekReadyWidget, - ), - ], + ); + }, ); }, ); diff --git a/lib/youtube/youtube_playlists_view.dart b/lib/youtube/youtube_playlists_view.dart index c2f88673..85515769 100644 --- a/lib/youtube/youtube_playlists_view.dart +++ b/lib/youtube/youtube_playlists_view.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:jiffy/jiffy.dart'; import 'package:playlist_manager/module/playlist_id.dart'; @@ -13,6 +12,7 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/class/youtube_id.dart'; import 'package:namida/youtube/controller/youtube_history_controller.dart'; @@ -24,27 +24,24 @@ import 'package:namida/youtube/pages/yt_playlist_subpage.dart'; import 'package:namida/youtube/widgets/yt_card.dart'; import 'package:namida/youtube/widgets/yt_history_video_card.dart'; import 'package:namida/youtube/yt_utils.dart'; +import 'package:playlist_manager/playlist_manager.dart'; class YoutubePlaylistsView extends StatelessWidget { final Iterable idsToAdd; final bool displayMenu; - final double bottomPadding; - final bool scrollable; final bool? minimalView; const YoutubePlaylistsView({ super.key, this.idsToAdd = const [], this.displayMenu = true, - this.bottomPadding = 0.0, - this.scrollable = true, this.minimalView, }); - Iterable get getHistoryVideos { + Iterable getHistoryVideos(Map> map) { final videos = {}; - for (final trs in YoutubeHistoryController.inst.historyMap.value.values) { - trs.loop((e, _) { + for (final trs in map.values) { + trs.loop((e) { videos[e.id] ??= e; }); if (videos.length >= 50) break; @@ -52,9 +49,9 @@ class YoutubePlaylistsView extends StatelessWidget { return videos.values; } - List get getFavouriteVideos { + List getFavouriteVideos(GeneralPlaylist playlist) { final videos = []; - final all = YoutubePlaylistController.inst.favouritesPlaylist.value.tracks; + final all = playlist.tracks; for (int i = all.length - 1; i >= 0; i--) { videos.add(all[i]); if (videos.length >= 50) break; @@ -64,19 +61,17 @@ class YoutubePlaylistsView extends StatelessWidget { } List getMenuItems(YoutubePlaylist playlist) { - return displayMenu - ? YTUtils.getVideosMenuItems( - videos: playlist.tracks, - playlistName: '', - moreItems: [ - NamidaPopupItem( - icon: Broken.trash, - title: lang.DELETE_PLAYLIST, - onTap: () => playlist.promptDelete(name: playlist.name), - ), - ], - ) - : []; + return YTUtils.getVideosMenuItems( + videos: playlist.tracks, + playlistName: '', + moreItems: [ + NamidaPopupItem( + icon: Broken.trash, + title: lang.DELETE_PLAYLIST, + onTap: () => playlist.promptDelete(name: playlist.name), + ), + ], + ); } @override @@ -87,6 +82,8 @@ class YoutubePlaylistsView extends StatelessWidget { const playlistThumbnailHeight = playlistsItemExtent - Dimensions.tileBottomMargin - (Dimensions.youtubeCardItemVerticalPadding * 2); const playlistThumbnailWidth = playlistThumbnailHeight * 16 / 9; + const yTMostPlayedVideosPage = YTMostPlayedVideosPage(); + return NamidaScrollbarWithController( child: (sc) => CustomScrollView( controller: sc, @@ -95,60 +92,61 @@ class YoutubePlaylistsView extends StatelessWidget { if (!isMinimalView) ...[ Obx( () { - YoutubeHistoryController.inst.historyMap.value; + final history = YoutubeHistoryController.inst.historyMap.valueR; + final length = YoutubeHistoryController.inst.totalHistoryItemsCount.valueR; + final lengthDummy = length == -1; return _HorizontalSliverList( title: lang.HISTORY, icon: Broken.refresh, - viewAllPage: const YoutubeHistoryPage(), - videos: getHistoryVideos, + viewAllPage: () => const YoutubeHistoryPage(), + videos: getHistoryVideos(history), playlistName: k_PLAYLIST_NAME_HISTORY, playlistID: k_PLAYLIST_NAME_HISTORY, displayTimeAgo: true, - totalVideosCountInMainList: YoutubeHistoryController.inst.historyTracksLength, - displayShimmer: YoutubeHistoryController.inst.isLoadingHistory, + totalVideosCountInMainList: lengthDummy ? 0 : length, + displayShimmer: lengthDummy, ); }, ), const SliverPadding(padding: EdgeInsets.only(bottom: 8.0)), - () { - const page = YTMostPlayedVideosPage(); - return Obx( - () { - YoutubeHistoryController.inst.historyMap.value; - return _HorizontalSliverList( - title: lang.MOST_PLAYED, - icon: Broken.crown_1, - viewAllPage: page, - padding: const EdgeInsets.only(top: 8.0), - videos: YoutubeHistoryController.inst.currentMostPlayedTracks - .map((e) => YoutubeID( - id: e, - playlistID: const PlaylistID(id: k_PLAYLIST_NAME_MOST_PLAYED), - )) - .toList(), - subHeader: page.getMainWidget(YoutubeHistoryController.inst.currentMostPlayedTracks.toList()).getChipsRow(context), - playlistName: '', - playlistID: k_PLAYLIST_NAME_MOST_PLAYED, - totalVideosCountInMainList: YoutubeHistoryController.inst.currentMostPlayedTracks.length, - displayShimmer: YoutubeHistoryController.inst.isLoadingHistory, - listensMap: YoutubeHistoryController.inst.currentTopTracksMapListens, - ); - }, - ); - }(), + Obx( + () { + final length = YoutubeHistoryController.inst.totalHistoryItemsCount.valueR; + final mostPlayed = YoutubeHistoryController.inst.currentMostPlayedTracks.toList(); + final listensMap = YoutubeHistoryController.inst.currentTopTracksMapListens.valueR; + return _HorizontalSliverList( + title: lang.MOST_PLAYED, + icon: Broken.crown_1, + viewAllPage: () => const YTMostPlayedVideosPage(), + padding: const EdgeInsets.only(top: 8.0), + videos: YoutubeHistoryController.inst.currentMostPlayedTracks + .map((e) => YoutubeID( + id: e, + playlistID: const PlaylistID(id: k_PLAYLIST_NAME_MOST_PLAYED), + )) + .toList(), + subHeader: yTMostPlayedVideosPage.getMainWidget(mostPlayed).getChipsRow(context), + playlistName: '', + playlistID: k_PLAYLIST_NAME_MOST_PLAYED, + totalVideosCountInMainList: mostPlayed.length, + displayShimmer: length == -1, + listensMap: listensMap, + ); + }, + ), const SliverPadding(padding: EdgeInsets.only(bottom: 8.0)), Obx( () { - YoutubePlaylistController.inst.favouritesPlaylist.value; + final favs = YoutubePlaylistController.inst.favouritesPlaylist.valueR; return _HorizontalSliverList( title: lang.LIKED, icon: Broken.like_1, - viewAllPage: const YTLikedVideosPage(), - videos: getFavouriteVideos, + viewAllPage: () => const YTLikedVideosPage(), + videos: getFavouriteVideos(favs), playlistName: k_PLAYLIST_NAME_FAV, playlistID: k_PLAYLIST_NAME_FAV, displayTimeAgo: false, - totalVideosCountInMainList: YoutubePlaylistController.inst.favouritesPlaylist.value.tracks.length, + totalVideosCountInMainList: favs.tracks.length, ); }, ), @@ -172,37 +170,36 @@ class YoutubePlaylistsView extends StatelessWidget { NamidaPopupWrapper( useRootNavigator: true, children: () => [ - Obx( - () { - final playlistSort = settings.ytPlaylistSort.value; - return Column( - children: [ - ListTileWithCheckMark( - active: settings.ytPlaylistSortReversed.value, - onTap: () => YoutubePlaylistController.inst.sortYTPlaylists(reverse: !settings.ytPlaylistSortReversed.value), - ), - ...[ - GroupSortType.title, - GroupSortType.creationDate, - GroupSortType.modifiedDate, - GroupSortType.numberOfTracks, - GroupSortType.shuffle, - ].map( - (e) => SmallListTile( - title: e.toText(), - active: playlistSort == e, - onTap: () => YoutubePlaylistController.inst.sortYTPlaylists(sortBy: e), - ), + Column( + children: [ + ListTileWithCheckMark( + activeRx: settings.ytPlaylistSortReversed, + onTap: () => YoutubePlaylistController.inst.sortYTPlaylists(reverse: !settings.ytPlaylistSortReversed.value), + ), + ...[ + GroupSortType.title, + GroupSortType.creationDate, + GroupSortType.modifiedDate, + GroupSortType.numberOfTracks, + GroupSortType.shuffle, + ].map( + (e) => ObxO( + rx: settings.ytPlaylistSort, + builder: (ytPlaylistSort) => SmallListTile( + title: e.toText(), + active: ytPlaylistSort == e, + onTap: () => YoutubePlaylistController.inst.sortYTPlaylists(sortBy: e), ), - ], - ); - }, + ), + ), + ], ), ], child: NamidaInkWell( - child: Obx( - () => Text( - settings.ytPlaylistSort.value.toText(), + child: ObxO( + rx: settings.ytPlaylistSort, + builder: (ytPlaylistSort) => Text( + ytPlaylistSort.toText(), style: context.textTheme.displaySmall?.copyWith( color: context.theme.colorScheme.secondary, ), @@ -212,12 +209,11 @@ class YoutubePlaylistsView extends StatelessWidget { ), const SizedBox(width: 4.0), NamidaInkWell( - onTap: () { - YoutubePlaylistController.inst.sortYTPlaylists(reverse: !settings.ytPlaylistSortReversed.value); - }, - child: Obx( - () => Icon( - settings.ytPlaylistSortReversed.value ? Broken.arrow_up_3 : Broken.arrow_down_2, + onTap: () => YoutubePlaylistController.inst.sortYTPlaylists(reverse: !settings.ytPlaylistSortReversed.value), + child: ObxO( + rx: settings.ytPlaylistSortReversed, + builder: (ytPlaylistSortReversed) => Icon( + ytPlaylistSortReversed ? Broken.arrow_up_3 : Broken.arrow_down_2, size: 16.0, color: context.theme.colorScheme.secondary, ), @@ -229,11 +225,12 @@ class YoutubePlaylistsView extends StatelessWidget { ), ), const SizedBox(width: 4.0), - Obx( - () => NamidaInkWellButton( + ObxO( + rx: YoutubeImportController.inst.isImportingPlaylists, + builder: (isImportingPlaylists) => NamidaInkWellButton( icon: Broken.add_circle, text: lang.IMPORT, - enabled: !YoutubeImportController.inst.isImportingPlaylists.value, + enabled: !isImportingPlaylists, onTap: () async { final dirPath = await NamidaFileBrowser.getDirectory(note: 'choose playlist directory from a google takeout'); if (dirPath != null) { @@ -255,7 +252,7 @@ class YoutubePlaylistsView extends StatelessWidget { const SliverPadding(padding: EdgeInsets.only(bottom: 8.0)), Obx( () { - final playlistsMap = YoutubePlaylistController.inst.playlistsMap; + final playlistsMap = YoutubePlaylistController.inst.playlistsMap.valueR; final playlistsNames = playlistsMap.keys.toList(); return SliverFixedExtentList.builder( itemExtent: playlistsItemExtent, @@ -263,67 +260,63 @@ class YoutubePlaylistsView extends StatelessWidget { itemBuilder: (context, index) { final name = playlistsNames[index]; final playlist = playlistsMap[name]!; + final idsExist = idsToAdd.isEmpty ? null : playlist.tracks.firstWhereEff((e) => e.id == idsToAdd.firstOrNull) != null; return NamidaPopupWrapper( - childrenDefault: () => getMenuItems(playlist), + childrenDefault: displayMenu ? () => getMenuItems(playlist) : null, openOnTap: false, - child: Obx( - () { - final idsExist = idsToAdd.isEmpty ? null : playlist.tracks.firstWhereEff((e) => e.id == idsToAdd.firstOrNull) != null; - return YoutubeCard( - isImageImportantInCache: true, - extractColor: true, - thumbnailWidthPercentage: 0.75, - videoId: playlist.tracks.firstOrNull?.id, - thumbnailUrl: null, - shimmerEnabled: false, - title: playlist.name, - subtitle: playlist.creationDate.dateFormattedOriginal, - displaythirdLineText: true, - thirdLineText: Jiffy.parseFromMillisecondsSinceEpoch(playlist.modifiedDate).fromNow(), - displayChannelThumbnail: false, - channelThumbnailUrl: '', - thumbnailHeight: playlistThumbnailHeight, - thumbnailWidth: playlistThumbnailWidth, - onTap: () { - if (idsToAdd.isNotEmpty) { - if (idsExist == true) { - final indexes = []; - playlist.tracks.loop((e, index) { - if (idsToAdd.contains(e.id)) { - indexes.add(index); - } - }); - NamidaNavigator.inst.navigateDialog( - dialog: CustomBlurryDialog( - isWarning: true, - normalTitleStyle: true, - bodyText: "${lang.REMOVE_FROM_PLAYLIST} ${playlist.name.addDQuotation()}?", - actions: [ - const CancelButton(), - const SizedBox(width: 6.0), - NamidaButton( - text: lang.REMOVE.toUpperCase(), - onPressed: () { - NamidaNavigator.inst.closeDialog(); - YoutubePlaylistController.inst.removeTracksFromPlaylist(playlist, indexes); - }, - ) - ], - ), - ); - } else { - YoutubePlaylistController.inst.addTracksToPlaylist(playlist, idsToAdd); + child: YoutubeCard( + isImageImportantInCache: true, + extractColor: true, + thumbnailWidthPercentage: 0.75, + videoId: playlist.tracks.firstOrNull?.id, + thumbnailUrl: null, + shimmerEnabled: false, + title: playlist.name, + subtitle: playlist.creationDate.dateFormattedOriginal, + displaythirdLineText: true, + thirdLineText: Jiffy.parseFromMillisecondsSinceEpoch(playlist.modifiedDate).fromNow(), + displayChannelThumbnail: false, + channelThumbnailUrl: '', + thumbnailHeight: playlistThumbnailHeight, + thumbnailWidth: playlistThumbnailWidth, + onTap: () { + if (idsToAdd.isNotEmpty) { + if (idsExist == true) { + final indexes = []; + playlist.tracks.loopAdv((e, index) { + if (idsToAdd.contains(e.id)) { + indexes.add(index); } - } else { - NamidaNavigator.inst.navigateTo(YTNormalPlaylistSubpage(playlistName: playlist.name)); - } - }, - smallBoxText: playlist.tracks.length.formatDecimal(), - smallBoxIcon: Broken.play_cricle, - checkmarkStatus: idsExist, - menuChildrenDefault: () => getMenuItems(playlist), - ); + }); + NamidaNavigator.inst.navigateDialog( + dialog: CustomBlurryDialog( + isWarning: true, + normalTitleStyle: true, + bodyText: "${lang.REMOVE_FROM_PLAYLIST} ${playlist.name.addDQuotation()}?", + actions: [ + const CancelButton(), + const SizedBox(width: 6.0), + NamidaButton( + text: lang.REMOVE.toUpperCase(), + onPressed: () { + NamidaNavigator.inst.closeDialog(); + YoutubePlaylistController.inst.removeTracksFromPlaylist(playlist, indexes); + }, + ) + ], + ), + ); + } else { + YoutubePlaylistController.inst.addTracksToPlaylist(playlist, idsToAdd); + } + } else { + NamidaNavigator.inst.navigateTo(YTNormalPlaylistSubpage(playlistName: playlist.name)); + } }, + smallBoxText: playlist.tracks.length.formatDecimal(), + smallBoxIcon: Broken.play_cricle, + checkmarkStatus: idsExist, + menuChildrenDefault: displayMenu ? () => getMenuItems(playlist) : null, ), ); }, @@ -340,7 +333,7 @@ class YoutubePlaylistsView extends StatelessWidget { class _HorizontalSliverList extends StatelessWidget { final String title; final IconData icon; - final Widget viewAllPage; + final Widget Function() viewAllPage; final Iterable videos; final String playlistName; final String playlistID; @@ -366,7 +359,7 @@ class _HorizontalSliverList extends StatelessWidget { this.listensMap, }); - void onTap() => NamidaNavigator.inst.navigateTo(viewAllPage); + void onTap() => NamidaNavigator.inst.navigateTo(viewAllPage()); @override Widget build(BuildContext context) { @@ -383,7 +376,7 @@ class _HorizontalSliverList extends StatelessWidget { NamidaInkWell( margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0), padding: padding, - onTap: () => NamidaNavigator.inst.navigateTo(viewAllPage), + onTap: onTap, child: Column( children: [ SearchPageTitleRow( diff --git a/lib/youtube/yt_miniplayer_comments_subpage.dart b/lib/youtube/yt_miniplayer_comments_subpage.dart index 172d79c8..2c53c0c4 100644 --- a/lib/youtube/yt_miniplayer_comments_subpage.dart +++ b/lib/youtube/yt_miniplayer_comments_subpage.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:namida/controller/connectivity.dart'; import 'package:namida/controller/navigator_controller.dart'; @@ -9,6 +8,7 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/packages/scroll_physics_modified.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/settings/extra_settings.dart'; @@ -36,78 +36,77 @@ class _YTMiniplayerCommentsSubpageState extends State Text( + [ + lang.COMMENTS, + if (totalCommentsCount != null) totalCommentsCount.formatDecimalShort(), + ].join(' • '), + style: context.textTheme.displayMedium, + textAlign: TextAlign.start, + ), + ), + const Spacer(), + NamidaIconButton( + tooltip: YoutubeController.inst.isCurrentCommentsFromCache ? lang.CACHE : null, + icon: Broken.refresh, + iconSize: 22.0, + onPressed: () async { + if (!ConnectivityController.inst.hasConnection) return; + sc.jumpTo(0); + await YoutubeController.inst.updateCurrentComments( + currentId ?? '', + forceRequest: ConnectivityController.inst.hasConnection, + ); + }, + child: YoutubeController.inst.isCurrentCommentsFromCache + ? const StackedIcon( + baseIcon: Broken.refresh, + secondaryIcon: Broken.global, + ) + : Icon( + Broken.refresh, + color: context.defaultIconColor(), + ), + ), + const SizedBox(width: 8.0), + ], + ), + ), ), Expanded( child: NamidaScrollbar( @@ -123,7 +122,7 @@ class _YTMiniplayerCommentsSubpageState extends State isLoadingComments + ? const SliverPadding( + padding: EdgeInsets.all(12.0), + sliver: SliverToBoxAdapter( + child: Center( child: LoadingIndicator(), - ).toSliver(), - ) - : const SizedBox().toSliver(); - }, + ), + ), + ) + : const SliverToBoxAdapter(child: SizedBox()), ), const SliverPadding(padding: EdgeInsets.only(bottom: kYTQueueSheetMinHeight + 12.0)) ], diff --git a/lib/youtube/yt_utils.dart b/lib/youtube/yt_utils.dart index 5f362842..8bdb7216 100644 --- a/lib/youtube/yt_utils.dart +++ b/lib/youtube/yt_utils.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_html/flutter_html.dart'; -import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; import 'package:playlist_manager/module/playlist_id.dart'; @@ -11,8 +10,8 @@ import 'package:share_plus/share_plus.dart'; import 'package:namida/class/video.dart'; import 'package:namida/controller/current_color.dart'; -import 'package:namida/controller/indexer_controller.dart'; import 'package:namida/controller/ffmpeg_controller.dart'; +import 'package:namida/controller/indexer_controller.dart'; import 'package:namida/controller/miniplayer_controller.dart'; import 'package:namida/controller/navigator_controller.dart'; import 'package:namida/controller/player_controller.dart'; @@ -24,6 +23,7 @@ import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/class/youtube_id.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; @@ -171,9 +171,10 @@ class YTUtils { required Map idsNamesLookup, String playlistName = '', YoutubeID? videoYTID, + bool copyUrl = false, }) { final playAfterVid = getPlayerAfterVideo(); - final isCurrentlyPlaying = Player.inst.nowPlayingVideoID != null && videoId == Player.inst.getCurrentVideoId; + final isCurrentlyPlaying = Player.inst.currentVideo != null && videoId == Player.inst.getCurrentVideoId; return [ NamidaPopupItem( icon: Broken.music_library_2, @@ -197,13 +198,19 @@ class YTUtils { title: lang.SHARE, onTap: () => Share.share(url), ), + if (copyUrl) + NamidaPopupItem( + icon: Broken.copy, + title: lang.COPY, + onTap: () => YTUtils().copyCurrentVideoUrl(videoId), + ), isCurrentlyPlaying ? NamidaPopupItem( icon: Broken.pause, title: lang.STOP_AFTER_THIS_VIDEO, - enabled: Player.inst.sleepAfterTracks != 1, + enabled: Player.inst.sleepTimerConfig.value.sleepAfterItems != 1, onTap: () { - Player.inst.updateSleepTimerValues(enableSleepAfterTracks: true, sleepAfterTracks: 1); + Player.inst.updateSleepTimerValues(enableSleepAfterItems: true, sleepAfterItems: 1); }, ) : NamidaPopupItem( @@ -259,11 +266,13 @@ class YTUtils { static ({YoutubeID video, int diff, String name})? getPlayerAfterVideo() { final player = Player.inst; - if (player.currentQueueYoutube.isNotEmpty && player.latestInsertedIndex != player.currentIndex) { - final playAfterVideo = player.currentQueueYoutube[player.latestInsertedIndex]; - final diff = player.latestInsertedIndex - player.currentIndex; - final name = YoutubeController.inst.getVideoName(playAfterVideo.id) ?? ''; - return (video: playAfterVideo, diff: diff, name: name); + if (player.currentItem.value is YoutubeID && player.latestInsertedIndex != player.currentIndex.value) { + try { + final playAfterVideo = player.currentQueue.value[player.latestInsertedIndex] as YoutubeID; + final diff = player.latestInsertedIndex - player.currentIndex.value; + final name = YoutubeController.inst.getVideoName(playAfterVideo.id) ?? ''; + return (video: playAfterVideo, diff: diff, name: name); + } catch (_) {} } return null; } @@ -354,13 +363,7 @@ class YTUtils { title: lang.UNDO_CHANGES, message: lang.UNDO_CHANGES_DELETED_TRACK, displaySeconds: 3, - button: TextButton( - onPressed: () { - Get.closeCurrentSnackbar(); - whatDoYouWant(); - }, - child: Text(lang.UNDO), - ), + button: (lang.UNDO, whatDoYouWant), ); } @@ -380,7 +383,7 @@ class YTUtils { if (playlist == null) return; final Map twdAndIndexes = {}; - videosToDelete.loop((twd, index) { + videosToDelete.loop((twd) { twdAndIndexes[twd] = playlist.tracks.indexOf(twd); }); @@ -406,13 +409,13 @@ class YTUtils { int videosSize = 0; int audiosSize = 0; - audiosCached.loop((e, _) { + audiosCached.loop((e) { final s = e.file.sizeInBytesSync(); audiosSize += s; fileSizeLookup[e.file.path] = s; fileTypeLookup[e.file.path] = 0; }); - videosCached.loop((e, _) { + videosCached.loop((e) { final s = e.sizeInBytes; videosSize += s; fileSizeLookup[e.path] = s; @@ -471,7 +474,7 @@ class YTUtils { required ({String title, String subtitle, String path}) Function(T item) itemBuilder, required int Function(T item) itemSize, required RxMap tempFilesSize, - required RxBool tempFilesDelete, + required Rx tempFilesDelete, }) { return NamidaExpansionTile( initiallyExpanded: true, @@ -553,7 +556,7 @@ class YTUtils { trailing: Obx( () => NamidaCheckMark( size: 16.0, - active: tempFilesDelete.value, + active: tempFilesDelete.valueR, ), ), ); @@ -586,12 +589,12 @@ class YTUtils { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(4.0.multipliedRadius), ), - value: allSelected.value, - onChanged: (value) { - allSelected.value = !allSelected.value; - if (allSelected.value == true) { - audiosCached.loop((e, _) => pathsToDelete[e.file.path] = true); - videosCached.loop((e, _) => pathsToDelete[e.path] = true); + value: allSelected.valueR, + onChanged: (_) { + final newVal = allSelected.toggle(); + if (newVal == true) { + audiosCached.loop((e) => pathsToDelete[e.file.path] = true); + videosCached.loop((e) => pathsToDelete[e.path] = true); deleteTempAudio.value = true; deleteTempVideo.value = true; } else { @@ -608,8 +611,8 @@ class YTUtils { const CancelButton(), Obx( () => NamidaButton( - enabled: deleteTempAudio.value || deleteTempVideo.value || pathsToDelete.values.any((element) => element), - text: "${lang.DELETE} (${totalSizeToDelete.value.fileSizeFormatted})", + enabled: deleteTempAudio.valueR || deleteTempVideo.valueR || pathsToDelete.values.any((element) => element), + text: "${lang.DELETE} (${totalSizeToDelete.valueR.fileSizeFormatted})", onPressed: () async { await Future.wait([ deleteItems(pathsToDelete.keys.where((element) => pathsToDelete[element] == true)), @@ -663,9 +666,9 @@ class YTUtils { ); } - void copyVideoUrl(String videoId) { + void copyCurrentVideoUrl(String videoId) { if (videoId != '') { - final atSeconds = Player.inst.nowPlayingPosition ~/ 1000; + final atSeconds = Player.inst.nowPlayingPosition.value ~/ 1000; final timeStamp = atSeconds > 0 ? '?t=$atSeconds' : ''; final finalUrl = "https://www.youtube.com/watch?v=$videoId$timeStamp"; Clipboard.setData(ClipboardData(text: finalUrl)); diff --git a/pubspec.yaml b/pubspec.yaml index 7969d3ba..ed515bd8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,40 +4,42 @@ publish_to: "none" version: 2.5.6+240522118 environment: - sdk: ">=3.1.4 <4.0.0" - flutter: ^3.13.8 + sdk: ">=3.4.0 <4.0.0" + flutter: ^3.22.0 dependency_overrides: - win32: ^3.0.0 just_audio_platform_interface: git: url: https://github.com/MSOB7YY/just_audio path: just_audio_platform_interface/ ref: video - archive: ^3.3.8 - intl: ^0.18.0 dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter # ---- Core ---- - get: + nampack: + git: + url: https://github.com/MSOB7YY/nampack + dart_extensions: git: - url: https://github.com/MSOB7YY/getx + url: https://github.com/MSOB7YY/dart_extensions permission_handler: ^11.0.1 logger: ^2.2.0 - package_info_plus: ^4.2.0 - device_info_plus: ^9.1.2 - intl: ^0.18.0 - flutter_archive: ^5.0.0 + package_info_plus: ^8.0.0 + device_info_plus: ^10.1.0 + intl: ^0.19.0 + flutter_archive: ^6.0.3 url_launcher: ^6.1.10 - connectivity_plus: ^4.0.2 + connectivity_plus: ^6.0.3 selectable_autolink_text: ^2.6.0 - share_plus: ^7.1.0 - calendar_date_picker2: ^0.5.2 + share_plus: ^9.0.0 + calendar_date_picker2: ^1.0.2 wakelock_plus: ^1.1.1 - flutter_local_notifications: ^15.1.0 + flutter_local_notifications: ^17.1.2 flutter_sharing_intent: git: url: https://github.com/MSOB7YY/flutter_sharing_intent @@ -53,11 +55,12 @@ dependencies: git: url: https://github.com/zhourengui/flutter_html flutter_volume_controller: ^1.3.0 - flutter_markdown: ^0.6.17+3 + flutter_markdown: ^0.7.1 flutter_mailer: ^2.1.1 lrc: ^1.0.2 vibration: ^1.8.4 flutter_displaymode: ^0.6.0 + native_device_orientation: ^2.0.3 # ---- Audio Indexing & Playback ---- just_audio: @@ -86,16 +89,13 @@ dependencies: # ---- Image Utilities ---- palette_generator: ^0.3.3+2 - photo_view: ^0.14.0 + photo_view: ^0.15.0 # ---- UI Rendering ---- flutter_staggered_animations: ^1.1.1 - flutter_staggered_grid_view: ^0.6.2 + flutter_staggered_grid_view: ^0.7.0 flutter_colorpicker: ^1.0.3 animated_background: ^2.0.0 - known_extents_list_view_builder: - git: - url: https://github.com/MSOB7YY/known_extents_listview_fancy flutter_animate: ^4.2.0+1 dynamic_color: ^1.6.7 scrollable_positioned_list: ^0.3.8 @@ -117,9 +117,15 @@ dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.2 + flutter_lints: ^4.0.0 flutter_native_splash: + custom_lint: + obx_lints: + git: + url: https://github.com/MSOB7YY/nampack + path: packages/obx_lints + flutter_native_splash: image: assets/namida_icon.png image_dark: assets/namida_icon.png