diff --git a/.github/workflows/spotube-publish-binary.yml b/.github/workflows/spotube-publish-binary.yml
index 85c26e01..12a2f99b 100644
--- a/.github/workflows/spotube-publish-binary.yml
+++ b/.github/workflows/spotube-publish-binary.yml
@@ -22,12 +22,12 @@ jobs:
runs-on: ubuntu-22.04
if: contains(inputs.jobs, 'flathub')
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
repository: KRTirtho/com.github.KRTirtho.Spotube
token: ${{ secrets.FLATHUB_TOKEN }}
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
path: spotube
@@ -50,7 +50,7 @@ jobs:
runs-on: ubuntu-22.04
if: contains(inputs.jobs, 'aur')
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: dsaltares/fetch-gh-release-asset@master
with:
diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml
index 31b0ebb6..cd94ef67 100644
--- a/.github/workflows/spotube-release-binary.yml
+++ b/.github/workflows/spotube-release-binary.yml
@@ -32,7 +32,7 @@ jobs:
windows:
runs-on: windows-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: subosito/flutter-action@v2.10.0
with:
cache: true
@@ -102,7 +102,7 @@ jobs:
linux:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: subosito/flutter-action@v2.10.0
with:
cache: true
@@ -191,7 +191,7 @@ jobs:
android:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: subosito/flutter-action@v2.10.0
with:
cache: true
@@ -266,7 +266,7 @@ jobs:
macos:
runs-on: macos-12
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: subosito/flutter-action@v2.10.0
with:
cache: true
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 59fc0f08..1f0a5e62 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -1,58 +1,64 @@
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- Sptube
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- spotube
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- LSRequiresIPhoneOS
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UIViewControllerBasedStatusBarAppearance
-
- NSAppTransportSecurity
-
- NSAllowsArbitraryLoads
-
- NSAllowsArbitraryLoadsForMedia
-
-
- CADisableMinimumFrameDurationOnPhone
-
- UIStatusBarHidden
-
-
-
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Sptube
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ spotube
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+ NSAllowsArbitraryLoadsForMedia
+
+
+ CADisableMinimumFrameDurationOnPhone
+
+ UIStatusBarHidden
+
+ NSPhotoLibraryUsageDescription
+ This app require access to the photo library
+ NSCameraUsageDescription
+ This app require access to the device camera
+ NSMicrophoneUsageDescription
+ This app does not require access to the device microphone
+
+
\ No newline at end of file
diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart
index 5503ebb3..4781050d 100644
--- a/lib/collections/spotube_icons.dart
+++ b/lib/collections/spotube_icons.dart
@@ -94,4 +94,7 @@ abstract class SpotubeIcons {
static const noWifi = FeatherIcons.wifiOff;
static const wifi = FeatherIcons.wifi;
static const window = Icons.window_rounded;
+ static const user = FeatherIcons.user;
+ static const edit = FeatherIcons.edit;
+ static const web = FeatherIcons.globe;
}
diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart
index a36be283..16692462 100644
--- a/lib/components/library/user_local_tracks.dart
+++ b/lib/components/library/user_local_tracks.dart
@@ -131,7 +131,7 @@ final localTracksProvider = FutureProvider>((ref) async {
class UserLocalTracks extends HookConsumerWidget {
const UserLocalTracks({Key? key}) : super(key: key);
- void playLocalTracks(
+ Future playLocalTracks(
WidgetRef ref,
List tracks, {
LocalTrack? currentTrack,
@@ -203,10 +203,10 @@ class UserLocalTracks extends HookConsumerWidget {
const SizedBox(width: 10),
FilledButton(
onPressed: trackSnapshot.value != null
- ? () {
+ ? () async {
if (trackSnapshot.value?.isNotEmpty == true) {
if (!isPlaylistPlaying) {
- playLocalTracks(
+ await playLocalTracks(
ref,
trackSnapshot.value!,
);
@@ -295,8 +295,8 @@ class UserLocalTracks extends HookConsumerWidget {
index: index,
track: track,
userPlaylist: false,
- onTap: () {
- playLocalTracks(
+ onTap: () async {
+ await playLocalTracks(
ref,
sortedTracks,
currentTrack: track,
diff --git a/lib/components/player/player_controls.dart b/lib/components/player/player_controls.dart
index 7ae4fa82..07a6b7ba 100644
--- a/lib/components/player/player_controls.dart
+++ b/lib/components/player/player_controls.dart
@@ -7,12 +7,12 @@ import 'package:palette_generator/palette_generator.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/collections/intents.dart';
import 'package:spotube/extensions/context.dart';
+import 'package:spotube/extensions/duration.dart';
import 'package:spotube/hooks/use_progress.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/audio_player/loop_mode.dart';
-import 'package:spotube/utils/primitive_utils.dart';
class PlayerControls extends HookConsumerWidget {
final PaletteGenerator? palette;
@@ -113,19 +113,6 @@ class PlayerControls extends HookConsumerWidget {
:progressStatic
) = useProgress(ref);
- final totalMinutes = PrimitiveUtils.zeroPadNumStr(
- duration.inMinutes.remainder(60),
- );
- final totalSeconds = PrimitiveUtils.zeroPadNumStr(
- duration.inSeconds.remainder(60),
- );
- final currentMinutes = PrimitiveUtils.zeroPadNumStr(
- position.inMinutes.remainder(60),
- );
- final currentSeconds = PrimitiveUtils.zeroPadNumStr(
- position.inSeconds.remainder(60),
- );
-
final progress = useState(
useMemoized(() => progressStatic, []),
);
@@ -173,8 +160,8 @@ class PlayerControls extends HookConsumerWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Text("$currentMinutes:$currentSeconds"),
- Text("$totalMinutes:$totalSeconds"),
+ Text(position.toHumanReadableString()),
+ Text(duration.toHumanReadableString()),
],
),
),
diff --git a/lib/components/player/player_overlay.dart b/lib/components/player/player_overlay.dart
index f4984ad2..889e6609 100644
--- a/lib/components/player/player_overlay.dart
+++ b/lib/components/player/player_overlay.dart
@@ -100,9 +100,13 @@ class PlayerOverlay extends HookConsumerWidget {
child: GestureDetector(
onTap: () =>
GoRouter.of(context).push("/player"),
- child: PlayerTrackDetails(
- albumArt: albumArt,
- color: textColor,
+ child: Container(
+ width: double.infinity,
+ color: Colors.transparent,
+ child: PlayerTrackDetails(
+ albumArt: albumArt,
+ color: textColor,
+ ),
),
),
),
@@ -114,7 +118,9 @@ class PlayerOverlay extends HookConsumerWidget {
SpotubeIcons.skipBack,
color: textColor,
),
- onPressed: playlistNotifier.previous,
+ onPressed: playlist.isFetching
+ ? null
+ : playlistNotifier.previous,
),
Consumer(
builder: (context, ref, _) {
@@ -143,7 +149,9 @@ class PlayerOverlay extends HookConsumerWidget {
SpotubeIcons.skipForward,
color: textColor,
),
- onPressed: playlistNotifier.next,
+ onPressed: playlist.isFetching
+ ? null
+ : playlistNotifier.next,
),
],
),
diff --git a/lib/components/player/player_queue.dart b/lib/components/player/player_queue.dart
index 599da26e..a5dee8c9 100644
--- a/lib/components/player/player_queue.dart
+++ b/lib/components/player/player_queue.dart
@@ -1,16 +1,21 @@
import 'dart:ui';
+import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/fallbacks/not_found.dart';
import 'package:spotube/components/shared/track_table/track_tile.dart';
+import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/use_auto_scroll_controller.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
+import 'package:spotube/utils/type_conversion_utils.dart';
class PlayerQueue extends HookConsumerWidget {
final bool floating;
@@ -24,12 +29,11 @@ class PlayerQueue extends HookConsumerWidget {
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
final controller = useAutoScrollController();
+ final searchText = useState('');
+
+ final isSearching = useState(false);
+
final tracks = playlist.tracks;
-
- if (tracks.isEmpty) {
- return const NotFound(vertical: true);
- }
-
final borderRadius = floating
? BorderRadius.circular(10)
: const BorderRadius.only(
@@ -39,6 +43,27 @@ class PlayerQueue extends HookConsumerWidget {
final theme = Theme.of(context);
final headlineColor = theme.textTheme.headlineSmall?.color;
+ final filteredTracks = useMemoized(
+ () {
+ if (searchText.value.isEmpty) {
+ return tracks;
+ }
+ return tracks
+ .map((e) => (
+ weightedRatio(
+ '${e.name!} - ${TypeConversionUtils.artists_X_String(e.artists!)}',
+ searchText.value,
+ ),
+ e
+ ))
+ .sorted((a, b) => b.$1.compareTo(a.$1))
+ .where((e) => e.$1 > 50)
+ .map((e) => e.$2)
+ .toList();
+ },
+ [tracks, searchText.value],
+ );
+
useEffect(() {
if (playlist.active == null) return null;
@@ -50,6 +75,10 @@ class PlayerQueue extends HookConsumerWidget {
return null;
}, []);
+ if (tracks.isEmpty) {
+ return const NotFound(vertical: true);
+ }
+
return BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 12.0,
@@ -64,89 +93,172 @@ class PlayerQueue extends HookConsumerWidget {
color: theme.scaffoldBackgroundColor.withOpacity(0.5),
borderRadius: borderRadius,
),
- child: Column(
- children: [
- Container(
- height: 5,
- width: 100,
- margin: const EdgeInsets.only(bottom: 5, top: 2),
- decoration: BoxDecoration(
- color: headlineColor,
- borderRadius: BorderRadius.circular(20),
- ),
- ),
- Row(
+ child: CallbackShortcuts(
+ bindings: {
+ LogicalKeySet(LogicalKeyboardKey.escape): () {
+ if (!isSearching.value) {
+ Navigator.of(context).pop();
+ }
+ isSearching.value = false;
+ searchText.value = '';
+ }
+ },
+ child: LayoutBuilder(builder: (context, constraints) {
+ return Column(
children: [
- const SizedBox(width: 10),
- Text(
- context.l10n.tracks_in_queue(tracks.length),
- style: TextStyle(
+ Container(
+ height: 5,
+ width: 100,
+ margin: const EdgeInsets.only(bottom: 5, top: 2),
+ decoration: BoxDecoration(
color: headlineColor,
- fontWeight: FontWeight.bold,
- fontSize: 18,
+ borderRadius: BorderRadius.circular(20),
),
),
- const Spacer(),
- FilledButton(
- style: FilledButton.styleFrom(
- backgroundColor:
- theme.scaffoldBackgroundColor.withOpacity(0.5),
- foregroundColor: theme.textTheme.headlineSmall?.color,
- ),
- child: Row(
- children: [
- const Icon(SpotubeIcons.playlistRemove),
- const SizedBox(width: 5),
- Text(context.l10n.clear_all),
- ],
- ),
- onPressed: () {
- playlistNotifier.stop();
- Navigator.of(context).pop();
- },
- ),
- const SizedBox(width: 10),
- ],
- ),
- const SizedBox(height: 10),
- Flexible(
- child: ReorderableListView.builder(
- onReorder: (oldIndex, newIndex) {
- playlistNotifier.moveTrack(oldIndex, newIndex);
- },
- scrollController: controller,
- itemCount: tracks.length,
- shrinkWrap: true,
- buildDefaultDragHandles: false,
- itemBuilder: (context, i) {
- final track = tracks.elementAt(i);
- return AutoScrollTag(
- key: ValueKey(i),
- controller: controller,
- index: i,
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 8.0),
- child: TrackTile(
- index: i,
- track: track,
- onTap: () async {
- if (playlist.activeTrack?.id == track.id) {
- return;
- }
- await playlistNotifier.jumpToTrack(track);
- },
- leadingActions: [
- ReorderableDragStartListener(
- index: i,
- child: const Icon(SpotubeIcons.dragHandle),
- ),
- ],
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ if (constraints.mdAndUp || !isSearching.value) ...[
+ const SizedBox(width: 10),
+ Text(
+ context.l10n.tracks_in_queue(tracks.length),
+ style: TextStyle(
+ color: headlineColor,
+ fontWeight: FontWeight.bold,
+ fontSize: 18,
),
),
- );
- }),
- ),
- ],
+ const Spacer(),
+ ],
+ if (constraints.mdAndUp || isSearching.value)
+ TextField(
+ onChanged: (value) {
+ searchText.value = value;
+ },
+ decoration: InputDecoration(
+ hintText: context.l10n.search,
+ isDense: true,
+ prefixIcon: constraints.smAndDown
+ ? IconButton(
+ icon: const Icon(
+ Icons.arrow_back_ios_new_outlined,
+ ),
+ onPressed: () {
+ isSearching.value = false;
+ searchText.value = '';
+ },
+ style: IconButton.styleFrom(
+ padding: EdgeInsets.zero,
+ minimumSize: const Size.square(20),
+ ),
+ )
+ : const Icon(SpotubeIcons.filter),
+ constraints: BoxConstraints(
+ maxHeight: 40,
+ maxWidth: constraints.smAndDown
+ ? constraints.maxWidth - 20
+ : 300,
+ ),
+ ),
+ )
+ else
+ IconButton.filledTonal(
+ icon: const Icon(SpotubeIcons.filter),
+ onPressed: () {
+ isSearching.value = !isSearching.value;
+ },
+ ),
+ if (constraints.mdAndUp || !isSearching.value) ...[
+ const SizedBox(width: 10),
+ FilledButton(
+ style: FilledButton.styleFrom(
+ backgroundColor:
+ theme.scaffoldBackgroundColor.withOpacity(0.5),
+ foregroundColor: theme.textTheme.headlineSmall?.color,
+ ),
+ child: Row(
+ children: [
+ const Icon(SpotubeIcons.playlistRemove),
+ const SizedBox(width: 5),
+ Text(context.l10n.clear_all),
+ ],
+ ),
+ onPressed: () {
+ playlistNotifier.stop();
+ Navigator.of(context).pop();
+ },
+ ),
+ const SizedBox(width: 10),
+ ],
+ ],
+ ),
+ const SizedBox(height: 10),
+ if (!isSearching.value && searchText.value.isEmpty)
+ Flexible(
+ child: ReorderableListView.builder(
+ onReorder: (oldIndex, newIndex) {
+ playlistNotifier.moveTrack(oldIndex, newIndex);
+ },
+ scrollController: controller,
+ itemCount: tracks.length,
+ shrinkWrap: true,
+ buildDefaultDragHandles: false,
+ itemBuilder: (context, i) {
+ final track = tracks.elementAt(i);
+ return AutoScrollTag(
+ key: ValueKey(i),
+ controller: controller,
+ index: i,
+ child: Padding(
+ padding:
+ const EdgeInsets.symmetric(horizontal: 8.0),
+ child: TrackTile(
+ index: i,
+ track: track,
+ onTap: () async {
+ if (playlist.activeTrack?.id == track.id) {
+ return;
+ }
+ await playlistNotifier.jumpToTrack(track);
+ },
+ leadingActions: [
+ ReorderableDragStartListener(
+ index: i,
+ child: const Icon(SpotubeIcons.dragHandle),
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ ),
+ )
+ else
+ Flexible(
+ child: ListView.builder(
+ itemCount: filteredTracks.length,
+ itemBuilder: (context, i) {
+ final track = filteredTracks.elementAt(i);
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 8.0),
+ child: TrackTile(
+ index: i,
+ track: track,
+ onTap: () async {
+ if (playlist.activeTrack?.id == track.id) {
+ return;
+ }
+ await playlistNotifier.jumpToTrack(track);
+ },
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ );
+ }),
),
),
);
diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/components/player/sibling_tracks_sheet.dart
index b7d802f9..d4857853 100644
--- a/lib/components/player/sibling_tracks_sheet.dart
+++ b/lib/components/player/sibling_tracks_sheet.dart
@@ -9,6 +9,7 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
+import 'package:spotube/extensions/duration.dart';
import 'package:spotube/hooks/use_debounce.dart';
import 'package:spotube/models/matched_track.dart';
import 'package:spotube/models/spotube_track.dart';
@@ -99,9 +100,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
- trailing: Text(
- PrimitiveUtils.toReadableDuration(video.duration),
- ),
+ trailing: Text(video.duration.toHumanReadableString()),
subtitle: Text(video.channelName),
enabled: playlist.isFetching != true,
selected: playlist.isFetching != true &&
diff --git a/lib/components/playlist/playlist_card.dart b/lib/components/playlist/playlist_card.dart
index be7abfb9..0438e559 100644
--- a/lib/components/playlist/playlist_card.dart
+++ b/lib/components/playlist/playlist_card.dart
@@ -32,6 +32,7 @@ class PlaylistCard extends HookConsumerWidget {
final updating = useState(false);
final spotify = ref.watch(spotifyProvider);
+ final me = useQueries.user.me(ref);
return PlaybuttonCard(
margin: const EdgeInsets.symmetric(horizontal: 10),
@@ -44,6 +45,7 @@ class PlaylistCard extends HookConsumerWidget {
isPlaying: isPlaylistPlaying,
isLoading:
(isPlaylistPlaying && playlistQueue.isFetching) || updating.value,
+ isOwner: playlist.owner?.id == me.data?.id && me.data?.id != null,
onTap: () {
ServiceUtils.push(
context,
@@ -60,11 +62,18 @@ class PlaylistCard extends HookConsumerWidget {
return audioPlayer.resume();
}
- List