mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-09 00:37:31 +00:00
Merge branch 'dev' into circleci-project-setup
This commit is contained in:
commit
73ba9f3a3d
6
.github/workflows/spotube-publish-binary.yml
vendored
6
.github/workflows/spotube-publish-binary.yml
vendored
@ -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:
|
||||
|
||||
8
.github/workflows/spotube-release-binary.yml
vendored
8
.github/workflows/spotube-release-binary.yml
vendored
@ -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
|
||||
|
||||
@ -1,58 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Sptube</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>spotube</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>NSAllowsArbitraryLoadsForMedia</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Sptube</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>spotube</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true />
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true />
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true />
|
||||
<key>NSAllowsArbitraryLoadsForMedia</key>
|
||||
<true />
|
||||
</dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true />
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false />
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app require access to the photo library</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app require access to the device camera</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>This app does not require access to the device microphone</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -131,7 +131,7 @@ final localTracksProvider = FutureProvider<List<LocalTrack>>((ref) async {
|
||||
class UserLocalTracks extends HookConsumerWidget {
|
||||
const UserLocalTracks({Key? key}) : super(key: key);
|
||||
|
||||
void playLocalTracks(
|
||||
Future<void> playLocalTracks(
|
||||
WidgetRef ref,
|
||||
List<LocalTrack> 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,
|
||||
|
||||
@ -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<num>(
|
||||
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()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -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);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -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 &&
|
||||
|
||||
@ -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<Track> fetchedTracks = await queryBowl.fetchQuery(
|
||||
"playlist-tracks/${playlist.id}",
|
||||
() => useQueries.playlist.tracksOf(playlist.id!, spotify),
|
||||
) ??
|
||||
[];
|
||||
List<Track> fetchedTracks = playlist.id == 'user-liked-tracks'
|
||||
? await queryBowl.fetchQuery(
|
||||
"user-liked-tracks",
|
||||
() => useQueries.playlist.likedTracks(spotify, ref),
|
||||
) ??
|
||||
[]
|
||||
: await queryBowl.fetchQuery(
|
||||
"playlist-tracks/${playlist.id}",
|
||||
() => useQueries.playlist
|
||||
.tracksOf(playlist.id!, spotify, ref),
|
||||
) ??
|
||||
[];
|
||||
|
||||
if (fetchedTracks.isEmpty) return;
|
||||
|
||||
@ -83,7 +92,7 @@ class PlaylistCard extends HookConsumerWidget {
|
||||
if (isPlaylistPlaying) return;
|
||||
List<Track> fetchedTracks = await queryBowl.fetchQuery(
|
||||
"playlist-tracks/${playlist.id}",
|
||||
() => useQueries.playlist.tracksOf(playlist.id!, spotify),
|
||||
() => useQueries.playlist.tracksOf(playlist.id!, spotify, ref),
|
||||
) ??
|
||||
[];
|
||||
|
||||
|
||||
@ -1,106 +1,276 @@
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:form_validator/form_validator.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
|
||||
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/provider/spotify_provider.dart';
|
||||
import 'package:spotube/services/mutations/mutations.dart';
|
||||
import 'package:spotube/services/mutations/playlist.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
class PlaylistCreateDialog extends HookConsumerWidget {
|
||||
/// Track ids to add to the playlist
|
||||
final List<String> trackIds;
|
||||
const PlaylistCreateDialog({
|
||||
final String? playlistId;
|
||||
PlaylistCreateDialog({
|
||||
Key? key,
|
||||
this.trackIds = const [],
|
||||
this.playlistId,
|
||||
}) : super(key: key);
|
||||
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final playlistName = useTextEditingController();
|
||||
final description = useTextEditingController();
|
||||
final public = useState(false);
|
||||
final collaborative = useState(false);
|
||||
final client = useQueryClient();
|
||||
final navigator = Navigator.of(context);
|
||||
return ScaffoldMessenger(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: HookBuilder(builder: (context) {
|
||||
final userPlaylists = useQueries.playlist.ofMine(ref);
|
||||
final updatingPlaylist = useMemoized(
|
||||
() => userPlaylists.pages
|
||||
.expand((p) => p.items ?? <PlaylistSimple>[])
|
||||
.firstWhereOrNull((playlist) => playlist.id == playlistId),
|
||||
[
|
||||
userPlaylists.pages,
|
||||
playlistId,
|
||||
],
|
||||
);
|
||||
|
||||
Future<void> onCreate() async {
|
||||
if (playlistName.text.isEmpty) return;
|
||||
final me = await spotify.me.get();
|
||||
final playlist = await spotify.playlists.createPlaylist(
|
||||
me.id!,
|
||||
playlistName.text,
|
||||
collaborative: collaborative.value,
|
||||
public: public.value,
|
||||
description: description.text,
|
||||
);
|
||||
if (trackIds.isNotEmpty) {
|
||||
await spotify.playlists.addTracks(
|
||||
trackIds.map((id) => "spotify:track:$id").toList(),
|
||||
playlist.id!,
|
||||
);
|
||||
}
|
||||
await client
|
||||
.getQuery(
|
||||
"current-user-playlists",
|
||||
)
|
||||
?.refresh();
|
||||
navigator.pop(playlist);
|
||||
}
|
||||
final playlistName = useTextEditingController(
|
||||
text: updatingPlaylist?.name,
|
||||
);
|
||||
final description = useTextEditingController(
|
||||
text: updatingPlaylist?.description,
|
||||
);
|
||||
final public = useState(
|
||||
updatingPlaylist?.public ?? false,
|
||||
);
|
||||
final collaborative = useState(
|
||||
updatingPlaylist?.collaborative ?? false,
|
||||
);
|
||||
final image = useState<XFile?>(null);
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(context.l10n.create_a_playlist),
|
||||
actions: [
|
||||
OutlinedButton(
|
||||
child: Text(context.l10n.cancel),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: onCreate,
|
||||
child: Text(context.l10n.create),
|
||||
),
|
||||
],
|
||||
content: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
constraints: const BoxConstraints(maxWidth: 500),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
TextField(
|
||||
controller: playlistName,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.name_of_playlist,
|
||||
labelText: context.l10n.name_of_playlist,
|
||||
final isUpdatingPlaylist = playlistId != null;
|
||||
|
||||
final l10n = context.l10n;
|
||||
final theme = Theme.of(context);
|
||||
final scaffold = ScaffoldMessenger.of(context);
|
||||
|
||||
final onError = useCallback((error) {
|
||||
if (error is SpotifyError || error is SpotifyException) {
|
||||
scaffold.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
l10n.error(error.message ?? "Epic failure!"),
|
||||
style: theme.textTheme.bodyMedium!.copyWith(
|
||||
color: theme.colorScheme.onError,
|
||||
),
|
||||
),
|
||||
backgroundColor: theme.colorScheme.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [scaffold, l10n, theme]);
|
||||
|
||||
final playlistCreateMutation = useMutations.playlist.create(
|
||||
ref,
|
||||
trackIds: trackIds,
|
||||
onData: (value) {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onError: onError,
|
||||
);
|
||||
|
||||
final playlistUpdateMutation = useMutations.playlist.update(
|
||||
ref,
|
||||
playlistId: playlistId,
|
||||
onData: (value) {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onError: onError,
|
||||
);
|
||||
|
||||
Future<void> onCreate() async {
|
||||
if (!formKey.currentState!.validate()) return;
|
||||
|
||||
final PlaylistCRUDVariables payload = (
|
||||
playlistName: playlistName.text,
|
||||
collaborative: collaborative.value,
|
||||
public: public.value,
|
||||
description: description.text,
|
||||
base64Image: image.value?.path != null
|
||||
? await image.value!
|
||||
.readAsBytes()
|
||||
.then((bytes) => base64Encode(bytes))
|
||||
: null,
|
||||
);
|
||||
|
||||
if (isUpdatingPlaylist) {
|
||||
await playlistUpdateMutation.mutate(payload);
|
||||
} else {
|
||||
await playlistCreateMutation.mutate(payload);
|
||||
}
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
isUpdatingPlaylist
|
||||
? context.l10n.update_playlist
|
||||
: context.l10n.create_a_playlist,
|
||||
),
|
||||
actions: [
|
||||
OutlinedButton(
|
||||
child: Text(context.l10n.cancel),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: onCreate,
|
||||
child: Text(
|
||||
isUpdatingPlaylist
|
||||
? context.l10n.update
|
||||
: context.l10n.create,
|
||||
),
|
||||
),
|
||||
],
|
||||
insetPadding: const EdgeInsets.all(8),
|
||||
content: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
constraints: const BoxConstraints(maxWidth: 500),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
FormField<XFile?>(
|
||||
initialValue: image.value,
|
||||
onSaved: (newValue) {
|
||||
image.value = newValue;
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null) return null;
|
||||
final file = File(value.path);
|
||||
|
||||
if (file.lengthSync() > 256000) {
|
||||
return "Image size should be less than 256kb";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
builder: (field) {
|
||||
return Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
UniversalImage(
|
||||
path: field.value?.path ??
|
||||
TypeConversionUtils.image_X_UrlString(
|
||||
updatingPlaylist?.images,
|
||||
placeholder:
|
||||
ImagePlaceholder.collection,
|
||||
),
|
||||
height: 200,
|
||||
),
|
||||
Positioned(
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
child: IconButton.filled(
|
||||
icon: const Icon(SpotubeIcons.edit),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor:
|
||||
theme.colorScheme.surface,
|
||||
foregroundColor:
|
||||
theme.colorScheme.primary,
|
||||
elevation: 2,
|
||||
shadowColor: theme.colorScheme.onSurface,
|
||||
),
|
||||
onPressed: () async {
|
||||
final imageFile = await ImagePicker()
|
||||
.pickImage(
|
||||
source: ImageSource.gallery);
|
||||
|
||||
if (imageFile != null) {
|
||||
field.didChange(imageFile);
|
||||
field.validate();
|
||||
field.save();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
if (field.hasError)
|
||||
Positioned(
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.error,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
field.errorText ?? "",
|
||||
style: theme.textTheme.bodyMedium!
|
||||
.copyWith(
|
||||
color: theme.colorScheme.onError,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
controller: playlistName,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.name_of_playlist,
|
||||
labelText: context.l10n.name_of_playlist,
|
||||
),
|
||||
validator: ValidationBuilder().required().build(),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
controller: description,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.description,
|
||||
),
|
||||
keyboardType: TextInputType.multiline,
|
||||
validator: ValidationBuilder().required().build(),
|
||||
maxLines: 5,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
CheckboxListTile(
|
||||
title: Text(context.l10n.public),
|
||||
value: public.value,
|
||||
onChanged: (val) => public.value = val ?? false,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
CheckboxListTile(
|
||||
title: Text(context.l10n.collaborative),
|
||||
value: collaborative.value,
|
||||
onChanged: (val) => collaborative.value = val ?? false,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: description,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.description,
|
||||
),
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: 5,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
CheckboxListTile(
|
||||
title: Text(context.l10n.public),
|
||||
value: public.value,
|
||||
onChanged: (val) => public.value = val ?? false,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
CheckboxListTile(
|
||||
title: Text(context.l10n.collaborative),
|
||||
value: collaborative.value,
|
||||
onChanged: (val) => collaborative.value = val ?? false,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -112,7 +282,7 @@ class PlaylistCreateDialogButton extends HookConsumerWidget {
|
||||
showPlaylistDialog(BuildContext context, SpotifyApi spotify) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => const PlaylistCreateDialog(),
|
||||
builder: (context) => PlaylistCreateDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -132,11 +302,12 @@ class PlaylistCreateDialogButton extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return FilledButton.tonalIcon(
|
||||
style: FilledButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
icon: const Icon(SpotubeIcons.addFilled),
|
||||
label: Text(context.l10n.create_playlist),
|
||||
onPressed: () => showPlaylistDialog(context, spotify));
|
||||
style: FilledButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
icon: const Icon(SpotubeIcons.addFilled),
|
||||
label: Text(context.l10n.create_playlist),
|
||||
onPressed: () => showPlaylistDialog(context, spotify),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,6 +78,31 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
|
||||
'Either icon or child must be provided',
|
||||
);
|
||||
|
||||
Future<T?> showPopupMenu(BuildContext context, RelativeRect position) {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
|
||||
return showMenu<T>(
|
||||
context: context,
|
||||
useRootNavigator: useRootNavigator,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: mediaQuery.size.height * 0.6,
|
||||
),
|
||||
position: position,
|
||||
items: children
|
||||
.map(
|
||||
(item) => PopupMenuItem<T>(
|
||||
padding: EdgeInsets.zero,
|
||||
enabled: false,
|
||||
child: _AdaptivePopSheetListItem<T>(
|
||||
item: item,
|
||||
onSelected: onSelected,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
|
||||
@ -29,7 +29,7 @@ class HeartButton extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||
|
||||
if (auth == null) return Container();
|
||||
if (auth == null) return const SizedBox.shrink();
|
||||
|
||||
return IconButton(
|
||||
tooltip: tooltip,
|
||||
@ -57,18 +57,21 @@ class HeartButton extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
({
|
||||
typedef UseTrackToggleLike = ({
|
||||
bool isLiked,
|
||||
Mutation<bool, dynamic, bool> toggleTrackLike,
|
||||
Query<User?, dynamic> me,
|
||||
}) useTrackToggleLike(Track track, WidgetRef ref) {
|
||||
});
|
||||
|
||||
UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) {
|
||||
final me = useQueries.user.me(ref);
|
||||
|
||||
final savedTracks =
|
||||
useQueries.playlist.tracksOfQuery(ref, "user-liked-tracks");
|
||||
final savedTracks = useQueries.playlist.likedTracksQuery(ref);
|
||||
|
||||
final isLiked =
|
||||
savedTracks.data?.any((element) => element.id == track.id) ?? false;
|
||||
final isLiked = useMemoized(
|
||||
() => savedTracks.data?.any((element) => element.id == track.id) ?? false,
|
||||
[savedTracks.data, track.id],
|
||||
);
|
||||
|
||||
final mounted = useIsMounted();
|
||||
|
||||
@ -76,28 +79,48 @@ class HeartButton extends HookConsumerWidget {
|
||||
ref,
|
||||
track.id!,
|
||||
onMutate: (isLiked) {
|
||||
savedTracks.setData(
|
||||
[
|
||||
if (isLiked == true)
|
||||
...?savedTracks.data?.where((element) => element.id != track.id)
|
||||
else
|
||||
...?savedTracks.data?..add(track)
|
||||
],
|
||||
);
|
||||
print("Toggle Like onMutate: $isLiked");
|
||||
|
||||
if (isLiked) {
|
||||
savedTracks.setData(
|
||||
savedTracks.data
|
||||
?.where((element) => element.id != track.id)
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
} else {
|
||||
savedTracks.setData(
|
||||
[
|
||||
...?savedTracks.data,
|
||||
track,
|
||||
],
|
||||
);
|
||||
}
|
||||
return isLiked;
|
||||
},
|
||||
onData: (data, recoveryData) async {
|
||||
print("Toggle Like onData: $data");
|
||||
await savedTracks.refresh();
|
||||
},
|
||||
onError: (payload, isLiked) {
|
||||
print("Toggle Like onError: $payload");
|
||||
if (!mounted()) return;
|
||||
|
||||
savedTracks.setData([
|
||||
if (isLiked != true)
|
||||
...?savedTracks.data?.where((element) => element.id != track.id)
|
||||
else
|
||||
...?savedTracks.data?..add(track),
|
||||
]);
|
||||
if (isLiked != true) {
|
||||
savedTracks.setData(
|
||||
savedTracks.data
|
||||
?.where((element) => element.id != track.id)
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
} else {
|
||||
savedTracks.setData(
|
||||
[
|
||||
...?savedTracks.data,
|
||||
track,
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@ -113,21 +136,21 @@ class TrackHeartButton extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final savedTracks =
|
||||
useQueries.playlist.tracksOfQuery(ref, "user-liked-tracks");
|
||||
final toggler = useTrackToggleLike(track, ref);
|
||||
if (toggler.me.isLoading || !toggler.me.hasData) {
|
||||
final savedTracks = useQueries.playlist.likedTracksQuery(ref);
|
||||
final (:me, :isLiked, :toggleTrackLike) = useTrackToggleLike(track, ref);
|
||||
|
||||
if (me.isLoading || !me.hasData) {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
|
||||
return HeartButton(
|
||||
tooltip: toggler.isLiked
|
||||
tooltip: isLiked
|
||||
? context.l10n.remove_from_favorites
|
||||
: context.l10n.save_as_favorite,
|
||||
isLiked: toggler.isLiked,
|
||||
isLiked: isLiked,
|
||||
onPressed: savedTracks.hasData
|
||||
? () {
|
||||
toggler.toggleTrackLike.mutate(toggler.isLiked);
|
||||
toggleTrackLike.mutate(isLiked);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
@ -136,10 +159,14 @@ class TrackHeartButton extends HookConsumerWidget {
|
||||
|
||||
class PlaylistHeartButton extends HookConsumerWidget {
|
||||
final PlaylistSimple playlist;
|
||||
final IconData? icon;
|
||||
final ValueChanged<bool>? onData;
|
||||
|
||||
const PlaylistHeartButton({
|
||||
required this.playlist,
|
||||
Key? key,
|
||||
this.icon,
|
||||
this.onData,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -158,6 +185,7 @@ class PlaylistHeartButton extends HookConsumerWidget {
|
||||
refreshQueries: [
|
||||
isLikedQuery.key,
|
||||
],
|
||||
onData: onData,
|
||||
);
|
||||
|
||||
if (me.isLoading || !me.hasData) {
|
||||
@ -170,6 +198,7 @@ class PlaylistHeartButton extends HookConsumerWidget {
|
||||
? context.l10n.remove_from_favorites
|
||||
: context.l10n.save_as_favorite,
|
||||
color: Colors.white,
|
||||
icon: icon,
|
||||
onPressed: isLikedQuery.hasData
|
||||
? () {
|
||||
togglePlaylistLike.mutate(isLikedQuery.data!);
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/shared/hover_builder.dart';
|
||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
import 'package:spotube/hooks/use_breakpoint_value.dart';
|
||||
import 'package:spotube/hooks/use_brightness_value.dart';
|
||||
@ -28,6 +29,7 @@ class PlaybuttonCard extends HookWidget {
|
||||
final bool isPlaying;
|
||||
final bool isLoading;
|
||||
final String title;
|
||||
final bool isOwner;
|
||||
|
||||
const PlaybuttonCard({
|
||||
required this.imageUrl,
|
||||
@ -39,6 +41,7 @@ class PlaybuttonCard extends HookWidget {
|
||||
this.onPlaybuttonPressed,
|
||||
this.onAddToQueuePressed,
|
||||
this.onTap,
|
||||
this.isOwner = false,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -153,6 +156,42 @@ class PlaybuttonCard extends HookWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isOwner)
|
||||
Positioned(
|
||||
top: 15,
|
||||
left: 25,
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
alignment: Alignment.centerLeft,
|
||||
curve: Curves.easeInExpo,
|
||||
child: HoverBuilder(builder: (context, isHovered) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blueAccent,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
SpotubeIcons.user,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
if (isHovered)
|
||||
Text(
|
||||
"Owned by you",
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
right: end,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@ -103,11 +104,19 @@ class TrackCollectionHeading<T> extends HookConsumerWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleLarge!.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: constrains.mdAndDown ? 400 : 300,
|
||||
),
|
||||
child: AutoSizeText(
|
||||
title,
|
||||
style: theme.textTheme.titleLarge!.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 2,
|
||||
minFontSize: 16,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (album != null)
|
||||
@ -125,11 +134,12 @@ class TrackCollectionHeading<T> extends HookConsumerWidget {
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: constrains.mdAndDown ? 400 : 300,
|
||||
),
|
||||
child: Text(
|
||||
child: AutoSizeText(
|
||||
cleanDescription,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.fade,
|
||||
minFontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/playlist/playlist_create_dialog.dart';
|
||||
import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart';
|
||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||
import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_heading.dart';
|
||||
@ -26,7 +29,7 @@ class TrackCollectionView<T> extends HookConsumerWidget {
|
||||
final Query<List<TrackSimple>, T> tracksSnapshot;
|
||||
final String titleImage;
|
||||
final PlayButtonState playingState;
|
||||
final void Function([Track? currentTrack]) onPlay;
|
||||
final Future<void> Function([Track? currentTrack]) onPlay;
|
||||
final void Function([Track? currentTrack]) onShuffledPlay;
|
||||
final void Function() onAddToQueue;
|
||||
final void Function() onShare;
|
||||
@ -71,6 +74,18 @@ class TrackCollectionView<T> extends HookConsumerWidget {
|
||||
icon: const Icon(SpotubeIcons.share),
|
||||
onPressed: onShare,
|
||||
),
|
||||
if (isOwned)
|
||||
IconButton(
|
||||
icon: const Icon(SpotubeIcons.edit),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return PlaylistCreateDialog(playlistId: id);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
if (heartBtn != null && auth != null) heartBtn!,
|
||||
IconButton(
|
||||
onPressed: playingState == PlayButtonState.playing
|
||||
|
||||
@ -14,7 +14,6 @@ import 'package:spotube/components/shared/heart_button.dart';
|
||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/local_track.dart';
|
||||
import 'package:spotube/models/spotube_track.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/blacklist_provider.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
@ -40,9 +39,11 @@ class TrackOptions extends HookConsumerWidget {
|
||||
final Track track;
|
||||
final bool userPlaylist;
|
||||
final String? playlistId;
|
||||
final ObjectRef<ValueChanged<RelativeRect>?>? showMenuCbRef;
|
||||
const TrackOptions({
|
||||
Key? key,
|
||||
required this.track,
|
||||
this.showMenuCbRef,
|
||||
this.userPlaylist = false,
|
||||
this.playlistId,
|
||||
}) : super(key: key);
|
||||
@ -114,210 +115,216 @@ class TrackOptions extends HookConsumerWidget {
|
||||
return downloadManager.getProgressNotifier(spotubeTrack);
|
||||
});
|
||||
|
||||
final adaptivePopSheetList = AdaptivePopSheetList<TrackOptionValue>(
|
||||
onSelected: (value) async {
|
||||
switch (value) {
|
||||
case TrackOptionValue.delete:
|
||||
await File((track as LocalTrack).path).delete();
|
||||
ref.refresh(localTracksProvider);
|
||||
break;
|
||||
case TrackOptionValue.addToQueue:
|
||||
await playback.addTrack(track);
|
||||
if (context.mounted) {
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.added_track_to_queue(track.name!),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case TrackOptionValue.playNext:
|
||||
playback.addTracksAtFirst([track]);
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.track_will_play_next(track.name!),
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case TrackOptionValue.removeFromQueue:
|
||||
playback.removeTrack(track.id!);
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.removed_track_from_queue(
|
||||
track.name!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case TrackOptionValue.favorite:
|
||||
favorites.toggleTrackLike.mutate(favorites.isLiked);
|
||||
break;
|
||||
case TrackOptionValue.addToPlaylist:
|
||||
actionAddToPlaylist(context, track);
|
||||
break;
|
||||
case TrackOptionValue.removeFromPlaylist:
|
||||
removingTrack.value = track.uri;
|
||||
removeTrack.mutate(track.uri!);
|
||||
break;
|
||||
case TrackOptionValue.blacklist:
|
||||
if (isBlackListed) {
|
||||
ref.read(BlackListNotifier.provider.notifier).remove(
|
||||
BlacklistedElement.track(track.id!, track.name!),
|
||||
);
|
||||
} else {
|
||||
ref.read(BlackListNotifier.provider.notifier).add(
|
||||
BlacklistedElement.track(track.id!, track.name!),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case TrackOptionValue.share:
|
||||
actionShare(context, track);
|
||||
break;
|
||||
case TrackOptionValue.details:
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => TrackDetailsDialog(track: track),
|
||||
);
|
||||
break;
|
||||
case TrackOptionValue.download:
|
||||
await downloadManager.addToQueue(track);
|
||||
break;
|
||||
}
|
||||
},
|
||||
icon: const Icon(SpotubeIcons.moreHorizontal),
|
||||
headings: [
|
||||
ListTile(
|
||||
dense: true,
|
||||
leading: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: UniversalImage(
|
||||
path: TypeConversionUtils.image_X_UrlString(track.album!.images,
|
||||
placeholder: ImagePlaceholder.albumArt),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
track.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
subtitle: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TypeConversionUtils.artists_X_ClickableArtists(
|
||||
track.artists!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
children: switch (track.runtimeType) {
|
||||
LocalTrack => [
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.delete,
|
||||
leading: const Icon(SpotubeIcons.trash),
|
||||
title: Text(context.l10n.delete),
|
||||
)
|
||||
],
|
||||
_ => [
|
||||
if (!playlist.containsTrack(track)) ...[
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.addToQueue,
|
||||
leading: const Icon(SpotubeIcons.queueAdd),
|
||||
title: Text(context.l10n.add_to_queue),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.playNext,
|
||||
leading: const Icon(SpotubeIcons.lightning),
|
||||
title: Text(context.l10n.play_next),
|
||||
),
|
||||
] else
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.removeFromQueue,
|
||||
enabled: playlist.activeTrack?.id != track.id,
|
||||
leading: const Icon(SpotubeIcons.queueRemove),
|
||||
title: Text(context.l10n.remove_from_queue),
|
||||
),
|
||||
if (favorites.me.hasData)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.favorite,
|
||||
leading: favorites.isLiked
|
||||
? const Icon(
|
||||
SpotubeIcons.heartFilled,
|
||||
color: Colors.pink,
|
||||
)
|
||||
: const Icon(SpotubeIcons.heart),
|
||||
title: Text(
|
||||
favorites.isLiked
|
||||
? context.l10n.remove_from_favorites
|
||||
: context.l10n.save_as_favorite,
|
||||
),
|
||||
),
|
||||
if (auth != null)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.addToPlaylist,
|
||||
leading: const Icon(SpotubeIcons.playlistAdd),
|
||||
title: Text(context.l10n.add_to_playlist),
|
||||
),
|
||||
if (userPlaylist && auth != null)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.removeFromPlaylist,
|
||||
leading: (removeTrack.isMutating || !removeTrack.hasData) &&
|
||||
removingTrack.value == track.uri
|
||||
? const CircularProgressIndicator()
|
||||
: const Icon(SpotubeIcons.removeFilled),
|
||||
title: Text(context.l10n.remove_from_playlist),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.download,
|
||||
enabled: !isInQueue,
|
||||
leading: isInQueue
|
||||
? HookBuilder(builder: (context) {
|
||||
final progress = useListenable(progressNotifier!);
|
||||
return CircularProgressIndicator(
|
||||
value: progress.value,
|
||||
);
|
||||
})
|
||||
: const Icon(SpotubeIcons.download),
|
||||
title: Text(context.l10n.download_track),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.blacklist,
|
||||
leading: const Icon(SpotubeIcons.playlistRemove),
|
||||
iconColor: !isBlackListed ? Colors.red[400] : null,
|
||||
textColor: !isBlackListed ? Colors.red[400] : null,
|
||||
title: Text(
|
||||
isBlackListed
|
||||
? context.l10n.remove_from_blacklist
|
||||
: context.l10n.add_to_blacklist,
|
||||
),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.share,
|
||||
leading: const Icon(SpotubeIcons.share),
|
||||
title: Text(context.l10n.share),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.details,
|
||||
leading: const Icon(SpotubeIcons.info),
|
||||
title: Text(context.l10n.details),
|
||||
),
|
||||
]
|
||||
},
|
||||
);
|
||||
|
||||
//! This is the most ANTI pattern I've ever done, but it works
|
||||
showMenuCbRef?.value = (relativeRect) {
|
||||
adaptivePopSheetList.showPopupMenu(context, relativeRect);
|
||||
};
|
||||
|
||||
return ListTileTheme(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: AdaptivePopSheetList<TrackOptionValue>(
|
||||
onSelected: (value) async {
|
||||
switch (value) {
|
||||
case TrackOptionValue.delete:
|
||||
await File((track as LocalTrack).path).delete();
|
||||
ref.refresh(localTracksProvider);
|
||||
break;
|
||||
case TrackOptionValue.addToQueue:
|
||||
await playback.addTrack(track);
|
||||
if (context.mounted) {
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.added_track_to_queue(track.name!),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case TrackOptionValue.playNext:
|
||||
playback.addTracksAtFirst([track]);
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.track_will_play_next(track.name!),
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case TrackOptionValue.removeFromQueue:
|
||||
playback.removeTrack(track.id!);
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.removed_track_from_queue(
|
||||
track.name!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case TrackOptionValue.favorite:
|
||||
favorites.toggleTrackLike.mutate(favorites.isLiked);
|
||||
break;
|
||||
case TrackOptionValue.addToPlaylist:
|
||||
actionAddToPlaylist(context, track);
|
||||
break;
|
||||
case TrackOptionValue.removeFromPlaylist:
|
||||
removingTrack.value = track.uri;
|
||||
removeTrack.mutate(track.uri!);
|
||||
break;
|
||||
case TrackOptionValue.blacklist:
|
||||
if (isBlackListed) {
|
||||
ref.read(BlackListNotifier.provider.notifier).remove(
|
||||
BlacklistedElement.track(track.id!, track.name!),
|
||||
);
|
||||
} else {
|
||||
ref.read(BlackListNotifier.provider.notifier).add(
|
||||
BlacklistedElement.track(track.id!, track.name!),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case TrackOptionValue.share:
|
||||
actionShare(context, track);
|
||||
break;
|
||||
case TrackOptionValue.details:
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => TrackDetailsDialog(track: track),
|
||||
);
|
||||
break;
|
||||
case TrackOptionValue.download:
|
||||
await downloadManager.addToQueue(track);
|
||||
break;
|
||||
}
|
||||
},
|
||||
icon: const Icon(SpotubeIcons.moreHorizontal),
|
||||
headings: [
|
||||
ListTile(
|
||||
dense: true,
|
||||
leading: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: UniversalImage(
|
||||
path: TypeConversionUtils.image_X_UrlString(
|
||||
track.album!.images,
|
||||
placeholder: ImagePlaceholder.albumArt),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
track.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
subtitle: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TypeConversionUtils.artists_X_ClickableArtists(
|
||||
track.artists!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
children: switch (track.runtimeType) {
|
||||
LocalTrack => [
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.delete,
|
||||
leading: const Icon(SpotubeIcons.trash),
|
||||
title: Text(context.l10n.delete),
|
||||
)
|
||||
],
|
||||
_ => [
|
||||
if (!playlist.containsTrack(track)) ...[
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.addToQueue,
|
||||
leading: const Icon(SpotubeIcons.queueAdd),
|
||||
title: Text(context.l10n.add_to_queue),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.playNext,
|
||||
leading: const Icon(SpotubeIcons.lightning),
|
||||
title: Text(context.l10n.play_next),
|
||||
),
|
||||
] else
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.removeFromQueue,
|
||||
enabled: playlist.activeTrack?.id != track.id,
|
||||
leading: const Icon(SpotubeIcons.queueRemove),
|
||||
title: Text(context.l10n.remove_from_queue),
|
||||
),
|
||||
if (favorites.me.hasData)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.favorite,
|
||||
leading: favorites.isLiked
|
||||
? const Icon(
|
||||
SpotubeIcons.heartFilled,
|
||||
color: Colors.pink,
|
||||
)
|
||||
: const Icon(SpotubeIcons.heart),
|
||||
title: Text(
|
||||
favorites.isLiked
|
||||
? context.l10n.remove_from_favorites
|
||||
: context.l10n.save_as_favorite,
|
||||
),
|
||||
),
|
||||
if (auth != null)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.addToPlaylist,
|
||||
leading: const Icon(SpotubeIcons.playlistAdd),
|
||||
title: Text(context.l10n.add_to_playlist),
|
||||
),
|
||||
if (userPlaylist && auth != null)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.removeFromPlaylist,
|
||||
leading: (removeTrack.isMutating || !removeTrack.hasData) &&
|
||||
removingTrack.value == track.uri
|
||||
? const CircularProgressIndicator()
|
||||
: const Icon(SpotubeIcons.removeFilled),
|
||||
title: Text(context.l10n.remove_from_playlist),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.download,
|
||||
enabled: !isInQueue,
|
||||
leading: isInQueue
|
||||
? HookBuilder(builder: (context) {
|
||||
final progress = useListenable(progressNotifier!);
|
||||
return CircularProgressIndicator(
|
||||
value: progress.value,
|
||||
);
|
||||
})
|
||||
: const Icon(SpotubeIcons.download),
|
||||
title: Text(context.l10n.download_track),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.blacklist,
|
||||
leading: const Icon(SpotubeIcons.playlistRemove),
|
||||
iconColor: !isBlackListed ? Colors.red[400] : null,
|
||||
textColor: !isBlackListed ? Colors.red[400] : null,
|
||||
title: Text(
|
||||
isBlackListed
|
||||
? context.l10n.remove_from_blacklist
|
||||
: context.l10n.add_to_blacklist,
|
||||
),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.share,
|
||||
leading: const Icon(SpotubeIcons.share),
|
||||
title: Text(context.l10n.share),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.details,
|
||||
leading: const Icon(SpotubeIcons.info),
|
||||
title: Text(context.l10n.details),
|
||||
),
|
||||
]
|
||||
},
|
||||
),
|
||||
child: adaptivePopSheetList,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@ -20,7 +23,7 @@ class TrackTile extends HookConsumerWidget {
|
||||
final Track track;
|
||||
final bool selected;
|
||||
final ValueChanged<bool?>? onChanged;
|
||||
final VoidCallback? onTap;
|
||||
final Future<void> Function()? onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
final bool userPlaylist;
|
||||
final String? playlistId;
|
||||
@ -57,174 +60,203 @@ class TrackTile extends HookConsumerWidget {
|
||||
[blacklist, track],
|
||||
);
|
||||
|
||||
final showOptionCbRef = useRef<ValueChanged<RelativeRect>?>(null);
|
||||
|
||||
final isPlaying = track.id == playlist.activeTrack?.id;
|
||||
|
||||
final isLoading = useState(false);
|
||||
|
||||
final isSelected = isPlaying || isLoading.value;
|
||||
|
||||
return LayoutBuilder(builder: (context, constrains) {
|
||||
return HoverBuilder(
|
||||
permanentState: isPlaying || constrains.smAndDown ? true : null,
|
||||
builder: (context, isHovering) {
|
||||
return ListTile(
|
||||
selected: isPlaying,
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
enabled: !isBlackListed,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
tileColor: isBlackListed ? theme.colorScheme.errorContainer : null,
|
||||
horizontalTitleGap: 12,
|
||||
leadingAndTrailingTextStyle: theme.textTheme.bodyMedium,
|
||||
leading: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...?leadingActions,
|
||||
if (index != null && onChanged == null && constrains.mdAndUp)
|
||||
SizedBox(
|
||||
width: 34,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: Text(
|
||||
'$index',
|
||||
maxLines: 1,
|
||||
style: theme.textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (constrains.smAndDown)
|
||||
const SizedBox(width: 16),
|
||||
if (onChanged != null)
|
||||
Checkbox(
|
||||
value: selected,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: UniversalImage(
|
||||
path: TypeConversionUtils.image_X_UrlString(
|
||||
track.album?.images,
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: isHovering
|
||||
? Colors.black.withOpacity(0.4)
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: IconTheme(
|
||||
data: theme.iconTheme
|
||||
.copyWith(size: 26, color: Colors.white),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: !isHovering
|
||||
? const SizedBox.shrink()
|
||||
: isPlaying && playlist.isFetching
|
||||
? const SizedBox(
|
||||
width: 26,
|
||||
height: 26,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1.5,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: isPlaying
|
||||
? Icon(
|
||||
SpotubeIcons.pause,
|
||||
color: theme.colorScheme.primary,
|
||||
)
|
||||
: const Icon(SpotubeIcons.play),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 6,
|
||||
child: Text(
|
||||
track.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (constrains.mdAndUp) ...[
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: switch (track.runtimeType) {
|
||||
LocalTrack => Text(
|
||||
track.album!.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
_ => Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: LinkText(
|
||||
track.album!.name!,
|
||||
"/album/${track.album?.id}",
|
||||
extra: track.album,
|
||||
push: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
subtitle: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: track is LocalTrack
|
||||
? Text(
|
||||
TypeConversionUtils.artists_X_String<Artist>(
|
||||
track.artists ?? [],
|
||||
),
|
||||
)
|
||||
: ClipRect(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 40),
|
||||
child: TypeConversionUtils.artists_X_ClickableArtists(
|
||||
track.artists ?? [],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
Duration(milliseconds: track.durationMs ?? 0)
|
||||
.toHumanReadableString(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
TrackOptions(
|
||||
track: track,
|
||||
playlistId: playlistId,
|
||||
userPlaylist: userPlaylist,
|
||||
),
|
||||
],
|
||||
return Listener(
|
||||
onPointerDown: (event) {
|
||||
if (event.buttons != kSecondaryMouseButton) return;
|
||||
showOptionCbRef.value?.call(
|
||||
RelativeRect.fromLTRB(
|
||||
event.position.dx,
|
||||
event.position.dy,
|
||||
constrains.maxWidth - event.position.dx,
|
||||
constrains.maxHeight - event.position.dy,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: HoverBuilder(
|
||||
permanentState: isSelected || constrains.smAndDown ? true : null,
|
||||
builder: (context, isHovering) {
|
||||
return ListTile(
|
||||
selected: isSelected,
|
||||
onTap: () async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
await onTap?.call();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
},
|
||||
onLongPress: onLongPress,
|
||||
enabled: !isBlackListed,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
tileColor:
|
||||
isBlackListed ? theme.colorScheme.errorContainer : null,
|
||||
horizontalTitleGap: 12,
|
||||
leadingAndTrailingTextStyle: theme.textTheme.bodyMedium,
|
||||
leading: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...?leadingActions,
|
||||
if (index != null && onChanged == null && constrains.mdAndUp)
|
||||
SizedBox(
|
||||
width: 34,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: Text(
|
||||
'$index',
|
||||
maxLines: 1,
|
||||
style: theme.textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (constrains.smAndDown)
|
||||
const SizedBox(width: 16),
|
||||
if (onChanged != null)
|
||||
Checkbox(
|
||||
value: selected,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: UniversalImage(
|
||||
path: TypeConversionUtils.image_X_UrlString(
|
||||
track.album?.images,
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: isHovering
|
||||
? Colors.black.withOpacity(0.4)
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: IconTheme(
|
||||
data: theme.iconTheme
|
||||
.copyWith(size: 26, color: Colors.white),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: (isPlaying && playlist.isFetching) ||
|
||||
isLoading.value
|
||||
? const SizedBox(
|
||||
width: 26,
|
||||
height: 26,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1.5,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: isPlaying
|
||||
? Icon(
|
||||
SpotubeIcons.pause,
|
||||
color: theme.colorScheme.primary,
|
||||
)
|
||||
: !isHovering
|
||||
? const SizedBox.shrink()
|
||||
: const Icon(SpotubeIcons.play),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 6,
|
||||
child: Text(
|
||||
track.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (constrains.mdAndUp) ...[
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: switch (track.runtimeType) {
|
||||
LocalTrack => Text(
|
||||
track.album!.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
_ => Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: LinkText(
|
||||
track.album!.name!,
|
||||
"/album/${track.album?.id}",
|
||||
extra: track.album,
|
||||
push: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
subtitle: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: track is LocalTrack
|
||||
? Text(
|
||||
TypeConversionUtils.artists_X_String<Artist>(
|
||||
track.artists ?? [],
|
||||
),
|
||||
)
|
||||
: ClipRect(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 40),
|
||||
child: TypeConversionUtils.artists_X_ClickableArtists(
|
||||
track.artists ?? [],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
Duration(milliseconds: track.durationMs ?? 0)
|
||||
.toHumanReadableString(padZero: false),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
TrackOptions(
|
||||
track: track,
|
||||
playlistId: playlistId,
|
||||
userPlaylist: userPlaylist,
|
||||
showMenuCbRef: showOptionCbRef,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
@ -27,7 +29,7 @@ final trackCollectionSortState =
|
||||
StateProvider.family<SortBy, String>((ref, _) => SortBy.none);
|
||||
|
||||
class TracksTableView extends HookConsumerWidget {
|
||||
final void Function(Track currentTrack)? onTrackPlayButtonPressed;
|
||||
final Future<void> Function(Track currentTrack)? onTrackPlayButtonPressed;
|
||||
final List<Track> tracks;
|
||||
final bool userPlaylist;
|
||||
final String? playlistId;
|
||||
@ -58,8 +60,7 @@ class TracksTableView extends HookConsumerWidget {
|
||||
final downloader = ref.watch(downloadManagerProvider.notifier);
|
||||
final apiType =
|
||||
ref.watch(userPreferencesProvider.select((s) => s.youtubeApiType));
|
||||
final tableHeadStyle =
|
||||
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
|
||||
const tableHeadStyle = TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
|
||||
|
||||
final selected = useState<List<String>>([]);
|
||||
final showCheck = useState<bool>(false);
|
||||
@ -297,7 +298,7 @@ class TracksTableView extends HookConsumerWidget {
|
||||
selected: selected.value.contains(track.id),
|
||||
userPlaylist: userPlaylist,
|
||||
playlistId: playlistId,
|
||||
onTap: () {
|
||||
onTap: () async {
|
||||
if (showCheck.value) {
|
||||
final alreadyChecked = selected.value.contains(track.id);
|
||||
if (alreadyChecked) {
|
||||
@ -314,9 +315,8 @@ class TracksTableView extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!isBlackListed) {
|
||||
onTrackPlayButtonPressed?.call(track);
|
||||
}
|
||||
if (isBlackListed) return;
|
||||
await onTrackPlayButtonPressed?.call(track);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
|
||||
@ -1,10 +1,21 @@
|
||||
import 'package:duration/locale.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
import 'package:duration/duration.dart';
|
||||
|
||||
extension DurationToHumanReadableString on Duration {
|
||||
String toHumanReadableString() =>
|
||||
"${inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(inSeconds.remainder(60))}";
|
||||
String toHumanReadableString({padZero = true}) {
|
||||
final mm = inMinutes
|
||||
.remainder(60)
|
||||
.toString()
|
||||
.padLeft(2, !padZero && inHours == 0 ? '' : "0");
|
||||
final ss = inSeconds.remainder(60).toString().padLeft(2, "0");
|
||||
|
||||
if (inHours > 0) {
|
||||
final hh = inHours.toString().padLeft(2, !padZero ? '' : "0");
|
||||
return "$hh:$mm:$ss";
|
||||
}
|
||||
|
||||
return "$mm:$ss";
|
||||
}
|
||||
|
||||
String format({
|
||||
DurationTersity tersity = DurationTersity.second,
|
||||
|
||||
@ -8,7 +8,6 @@
|
||||
|
||||
import 'package:audio_service_web/audio_service_web.dart';
|
||||
import 'package:audio_session/audio_session_web.dart';
|
||||
import 'package:file_picker/_internal/file_picker_web.dart';
|
||||
import 'package:shared_preferences_web/shared_preferences_web.dart';
|
||||
import 'package:url_launcher_web/url_launcher_web.dart';
|
||||
|
||||
@ -18,7 +17,6 @@ import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
||||
void registerPlugins(Registrar registrar) {
|
||||
AudioServiceWeb.registerWith(registrar);
|
||||
AudioSessionWeb.registerWith(registrar);
|
||||
FilePickerWeb.registerWith(registrar);
|
||||
SharedPreferencesPlugin.registerWith(registrar);
|
||||
UrlLauncherPlugin.registerWith(registrar);
|
||||
registrar.registerMessageHandler();
|
||||
|
||||
@ -4,7 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotube/hooks/use_async_effect.dart';
|
||||
|
||||
bool _asked = false;
|
||||
void useDisableBatterOptimizations() {
|
||||
void useDisableBatteryOptimizations() {
|
||||
useAsyncEffect(() async {
|
||||
if (!DesktopTools.platform.isAndroid || _asked) return;
|
||||
final localStorage = await SharedPreferences.getInstance();
|
||||
|
||||
@ -40,14 +40,14 @@ import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
}
|
||||
});
|
||||
|
||||
var lastPosition = position.value;
|
||||
|
||||
// audioPlayer.positionStream is fired every 200ms and only 1s delay is
|
||||
// enough. Thus only update the position if the difference is more than 1s
|
||||
// Reduces CPU usage
|
||||
var lastPosition = position.value;
|
||||
|
||||
final positionSubscription = audioPlayer.positionStream.listen((event) {
|
||||
if (event.inMilliseconds > 1000 &&
|
||||
event.inMilliseconds - lastPosition.inMilliseconds < 1000) return;
|
||||
final diff = event.inMilliseconds - lastPosition.inMilliseconds;
|
||||
if (event.inMilliseconds > 1000 && diff < 1000 && diff > 0) return;
|
||||
|
||||
lastPosition = event;
|
||||
position.value = event;
|
||||
|
||||
@ -24,8 +24,10 @@
|
||||
"liked_tracks_description": "All your liked tracks",
|
||||
"create_playlist": "Create Playlist",
|
||||
"create_a_playlist": "Create a playlist",
|
||||
"update_playlist": "Update playlist",
|
||||
"create": "Create",
|
||||
"cancel": "Cancel",
|
||||
"update": "Update",
|
||||
"playlist_name": "Playlist Name",
|
||||
"name_of_playlist": "Name of the playlist",
|
||||
"description": "Description",
|
||||
@ -259,5 +261,7 @@
|
||||
"piped_down_error_instructions": "The Piped instance {pipedInstance} is currently down\n\nEither change the instance or change the 'API type' to official YouTube API\n\nMake sure to restart the app after change",
|
||||
"you_are_offline": "You are currently offline",
|
||||
"connection_restored": "Your internet connection was restored",
|
||||
"use_system_title_bar": "Use system title bar"
|
||||
"use_system_title_bar": "Use system title bar",
|
||||
"crunching_results": "Crunching results...",
|
||||
"search_to_get_results": "Search to get results"
|
||||
}
|
||||
@ -1,9 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:catcher/catcher.dart';
|
||||
import 'package:device_preview/device_preview.dart';
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_devtools/fl_query_devtools.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -14,7 +12,6 @@ import 'package:hive/hive.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:metadata_god/metadata_god.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotube/collections/routes.dart';
|
||||
import 'package:spotube/collections/intents.dart';
|
||||
@ -26,6 +23,7 @@ import 'package:spotube/models/skip_segment.dart';
|
||||
import 'package:spotube/provider/palette_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/cli/cli.dart';
|
||||
import 'package:spotube/services/connectivity_adapter.dart';
|
||||
import 'package:spotube/themes/theme.dart';
|
||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||
@ -37,41 +35,7 @@ import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
|
||||
Future<void> main(List<String> rawArgs) async {
|
||||
final parser = ArgParser();
|
||||
|
||||
parser.addFlag(
|
||||
'verbose',
|
||||
abbr: 'v',
|
||||
help: 'Verbose mode',
|
||||
defaultsTo: !kReleaseMode,
|
||||
callback: (verbose) {
|
||||
if (verbose) {
|
||||
logEnv['VERBOSE'] = 'true';
|
||||
logEnv['DEBUG'] = 'true';
|
||||
logEnv['ERROR'] = 'true';
|
||||
}
|
||||
},
|
||||
);
|
||||
parser.addFlag(
|
||||
"version",
|
||||
help: "Print version and exit",
|
||||
negatable: false,
|
||||
);
|
||||
|
||||
parser.addFlag("help", abbr: "h", negatable: false);
|
||||
|
||||
final arguments = parser.parse(rawArgs);
|
||||
|
||||
if (arguments["help"] == true) {
|
||||
print(parser.usage);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (arguments["version"] == true) {
|
||||
final package = await PackageInfo.fromPlatform();
|
||||
print("Spotube v${package.version}");
|
||||
exit(0);
|
||||
}
|
||||
final arguments = await startCLI(rawArgs);
|
||||
|
||||
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
@ -215,7 +179,7 @@ class SpotubeState extends ConsumerState<Spotube> {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useDisableBatterOptimizations();
|
||||
useDisableBatteryOptimizations();
|
||||
|
||||
final lightTheme = useMemoized(
|
||||
() => theme(paletteColor ?? accentMaterialColor, Brightness.light),
|
||||
|
||||
@ -85,10 +85,10 @@ class AlbumPage extends HookConsumerWidget {
|
||||
album: album,
|
||||
routePath: "/album/${album.id}",
|
||||
bottomSpace: mediaQuery.mdAndDown,
|
||||
onPlay: ([track]) {
|
||||
onPlay: ([track]) async {
|
||||
if (tracksSnapshot.hasData) {
|
||||
if (!isAlbumPlaying) {
|
||||
playPlaylist(
|
||||
await playPlaylist(
|
||||
tracksSnapshot.data!
|
||||
.map((track) =>
|
||||
TypeConversionUtils.simpleTrack_X_Track(track, album))
|
||||
@ -96,7 +96,7 @@ class AlbumPage extends HookConsumerWidget {
|
||||
ref,
|
||||
);
|
||||
} else if (isAlbumPlaying && track != null) {
|
||||
playPlaylist(
|
||||
await playPlaylist(
|
||||
tracksSnapshot.data!
|
||||
.map((track) =>
|
||||
TypeConversionUtils.simpleTrack_X_Track(track, album))
|
||||
@ -105,7 +105,7 @@ class AlbumPage extends HookConsumerWidget {
|
||||
ref,
|
||||
);
|
||||
} else {
|
||||
playback
|
||||
await playback
|
||||
.removeTracks(tracksSnapshot.data!.map((track) => track.id!));
|
||||
}
|
||||
}
|
||||
|
||||
@ -390,7 +390,7 @@ class ArtistPage extends HookConsumerWidget {
|
||||
return TrackTile(
|
||||
index: i,
|
||||
track: track,
|
||||
onTap: () {
|
||||
onTap: () async {
|
||||
playPlaylist(
|
||||
topTracks.toList(),
|
||||
currentTrack: track,
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/shared/heart_button.dart';
|
||||
import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_heading.dart';
|
||||
import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_view.dart';
|
||||
@ -18,34 +20,8 @@ import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
class PlaylistView extends HookConsumerWidget {
|
||||
final logger = getLogger(PlaylistView);
|
||||
final PlaylistSimple playlist;
|
||||
PlaylistView(this.playlist, {Key? key}) : super(key: key);
|
||||
|
||||
Future<void> playPlaylist(
|
||||
List<Track> tracks,
|
||||
WidgetRef ref, {
|
||||
Track? currentTrack,
|
||||
}) async {
|
||||
final proxyPlaylist = ref.read(ProxyPlaylistNotifier.provider);
|
||||
final playback = ref.read(ProxyPlaylistNotifier.notifier);
|
||||
final sortBy = ref.read(trackCollectionSortState(playlist.id!));
|
||||
final sortedTracks = ServiceUtils.sortTracks(tracks, sortBy);
|
||||
currentTrack ??= sortedTracks.first;
|
||||
final isPlaylistPlaying = proxyPlaylist.containsTracks(tracks);
|
||||
if (!isPlaylistPlaying) {
|
||||
playback.addCollection(playlist.id!); // for enabling loading indicator
|
||||
await playback.load(
|
||||
sortedTracks,
|
||||
initialIndex: sortedTracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||
autoPlay: true,
|
||||
);
|
||||
playback.addCollection(playlist.id!);
|
||||
} else if (isPlaylistPlaying &&
|
||||
currentTrack.id != null &&
|
||||
currentTrack.id != proxyPlaylist.activeTrack?.id) {
|
||||
await playback.jumpToTrack(currentTrack);
|
||||
}
|
||||
}
|
||||
final PlaylistSimple playlistSimple;
|
||||
PlaylistView(this.playlistSimple, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
@ -55,7 +31,16 @@ class PlaylistView extends HookConsumerWidget {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
|
||||
final meSnapshot = useQueries.user.me(ref);
|
||||
final tracksSnapshot = useQueries.playlist.tracksOfQuery(ref, playlist.id!);
|
||||
|
||||
final playlistQuery = useQueries.playlist.byId(ref, playlistSimple.id!);
|
||||
final playlist = playlistQuery.data ?? playlistSimple;
|
||||
|
||||
final playlistTrackSnapshot =
|
||||
useQueries.playlist.tracksOfQuery(ref, playlist.id!);
|
||||
final likedTracksSnapshot = useQueries.playlist.likedTracksQuery(ref);
|
||||
final tracksSnapshot = playlist.id! == "user-liked-tracks"
|
||||
? likedTracksSnapshot
|
||||
: playlistTrackSnapshot;
|
||||
|
||||
final isPlaylistPlaying = useMemoized(
|
||||
() => proxyPlaylist.collections.contains(playlist.id!),
|
||||
@ -78,6 +63,35 @@ class PlaylistView extends HookConsumerWidget {
|
||||
[proxyPlaylist.activeTrack, tracksSnapshot.data],
|
||||
);
|
||||
|
||||
final playPlaylist = useCallback((
|
||||
List<Track> tracks,
|
||||
WidgetRef ref, {
|
||||
Track? currentTrack,
|
||||
}) async {
|
||||
final playback = ref.read(ProxyPlaylistNotifier.notifier);
|
||||
final sortBy = ref.read(trackCollectionSortState(playlist.id!));
|
||||
final sortedTracks = ServiceUtils.sortTracks(tracks, sortBy);
|
||||
currentTrack ??= sortedTracks.first;
|
||||
final isPlaylistPlaying = proxyPlaylist.containsTracks(tracks);
|
||||
if (!isPlaylistPlaying) {
|
||||
playback.addCollection(playlist.id!); // for enabling loading indicator
|
||||
await playback.load(
|
||||
sortedTracks,
|
||||
initialIndex:
|
||||
sortedTracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||
autoPlay: true,
|
||||
);
|
||||
playback.addCollection(playlist.id!);
|
||||
} else if (isPlaylistPlaying &&
|
||||
currentTrack.id != null &&
|
||||
currentTrack.id != proxyPlaylist.activeTrack?.id) {
|
||||
await playback.jumpToTrack(currentTrack);
|
||||
}
|
||||
}, [proxyPlaylist, playlist]);
|
||||
|
||||
final ownPlaylist =
|
||||
playlist.owner?.id != null && playlist.owner?.id == meSnapshot.data?.id;
|
||||
|
||||
return TrackCollectionView(
|
||||
id: playlist.id!,
|
||||
playingState: isPlaylistPlaying && playlistTrackPlaying
|
||||
@ -89,24 +103,17 @@ class PlaylistView extends HookConsumerWidget {
|
||||
titleImage: titleImage,
|
||||
tracksSnapshot: tracksSnapshot,
|
||||
description: playlist.description,
|
||||
isOwned: playlist.owner?.id != null &&
|
||||
playlist.owner!.id == meSnapshot.data?.id,
|
||||
onPlay: ([track]) {
|
||||
isOwned: ownPlaylist,
|
||||
onPlay: ([track]) async {
|
||||
if (tracksSnapshot.hasData) {
|
||||
if (!isPlaylistPlaying) {
|
||||
playPlaylist(
|
||||
tracksSnapshot.data!,
|
||||
ref,
|
||||
currentTrack: track,
|
||||
);
|
||||
} else if (isPlaylistPlaying && track != null) {
|
||||
playPlaylist(
|
||||
if (!isPlaylistPlaying || (isPlaylistPlaying && track != null)) {
|
||||
await playPlaylist(
|
||||
tracksSnapshot.data!,
|
||||
ref,
|
||||
currentTrack: track,
|
||||
);
|
||||
} else {
|
||||
playlistNotifier
|
||||
await playlistNotifier
|
||||
.removeTracks(tracksSnapshot.data!.map((e) => e.id!));
|
||||
}
|
||||
}
|
||||
@ -137,7 +144,13 @@ class PlaylistView extends HookConsumerWidget {
|
||||
);
|
||||
});
|
||||
},
|
||||
heartBtn: PlaylistHeartButton(playlist: playlist),
|
||||
heartBtn: PlaylistHeartButton(
|
||||
playlist: playlist,
|
||||
icon: ownPlaylist ? SpotubeIcons.trash : null,
|
||||
onData: (data) {
|
||||
GoRouter.of(context).pop();
|
||||
},
|
||||
),
|
||||
onShuffledPlay: ([track]) {
|
||||
final tracks = [...?tracksSnapshot.data]..shuffle();
|
||||
|
||||
|
||||
@ -55,13 +55,295 @@ class SearchPage extends HookConsumerWidget {
|
||||
|
||||
Future<void> onSearch() async {
|
||||
await Future.wait([
|
||||
searchTrack.refreshAll(),
|
||||
searchAlbum.refreshAll(),
|
||||
searchPlaylist.refreshAll(),
|
||||
searchArtist.refreshAll(),
|
||||
]);
|
||||
searchTrack.reset(),
|
||||
searchAlbum.reset(),
|
||||
searchPlaylist.reset(),
|
||||
searchArtist.reset(),
|
||||
]).then((_) {
|
||||
return Future.wait([
|
||||
searchTrack.refreshAll(),
|
||||
searchAlbum.refreshAll(),
|
||||
searchPlaylist.refreshAll(),
|
||||
searchArtist.refreshAll(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
final queries = [searchTrack, searchAlbum, searchPlaylist, searchArtist];
|
||||
final isFetching = queries.every(
|
||||
(s) => s.isLoadingPage || s.isRefreshingPage || !s.hasPageData,
|
||||
) &&
|
||||
searchTerm.isNotEmpty;
|
||||
|
||||
final resultWidget = HookBuilder(
|
||||
builder: (context) {
|
||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||
List<AlbumSimple> albums = [];
|
||||
List<Artist> artists = [];
|
||||
List<Track> tracks = [];
|
||||
List<PlaylistSimple> playlists = [];
|
||||
final pages = [
|
||||
...searchTrack.pages,
|
||||
...searchAlbum.pages,
|
||||
...searchPlaylist.pages,
|
||||
...searchArtist.pages,
|
||||
].expand<Page>((page) => page).toList();
|
||||
for (MapEntry<int, Page> page in pages.asMap().entries) {
|
||||
for (var item in page.value.items ?? []) {
|
||||
if (item is AlbumSimple) {
|
||||
albums.add(item);
|
||||
} else if (item is PlaylistSimple) {
|
||||
playlists.add(item);
|
||||
} else if (item is Artist) {
|
||||
artists.add(item);
|
||||
} else if (item is Track) {
|
||||
tracks.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (tracks.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
context.l10n.songs,
|
||||
style: theme.textTheme.titleLarge!,
|
||||
),
|
||||
),
|
||||
if (searchTrack.isLoadingPage)
|
||||
const CircularProgressIndicator()
|
||||
else if (searchTrack.hasPageError)
|
||||
Text(
|
||||
searchTrack.errors.lastOrNull?.toString() ?? "",
|
||||
)
|
||||
else
|
||||
...tracks.mapIndexed((i, track) {
|
||||
return TrackTile(
|
||||
index: i,
|
||||
track: track,
|
||||
onTap: () async {
|
||||
final isTrackPlaying =
|
||||
playlist.activeTrack?.id == track.id;
|
||||
if (!isTrackPlaying && context.mounted) {
|
||||
final shouldPlay = (playlist.tracks.length) > 20
|
||||
? await showPromptDialog(
|
||||
context: context,
|
||||
title: context.l10n.playing_track(
|
||||
track.name!,
|
||||
),
|
||||
message: context.l10n.queue_clear_alert(
|
||||
playlist.tracks.length,
|
||||
),
|
||||
)
|
||||
: true;
|
||||
|
||||
if (shouldPlay) {
|
||||
await playlistNotifier.load(
|
||||
[track],
|
||||
autoPlay: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
if (searchTrack.hasNextPage && tracks.isNotEmpty)
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: searchTrack.isRefreshingPage
|
||||
? null
|
||||
: () => searchTrack.fetchNext(),
|
||||
child: searchTrack.isRefreshingPage
|
||||
? const CircularProgressIndicator()
|
||||
: Text(context.l10n.load_more),
|
||||
),
|
||||
),
|
||||
if (playlists.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
context.l10n.playlists,
|
||||
style: theme.textTheme.titleLarge!,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.mouse,
|
||||
},
|
||||
),
|
||||
child: Scrollbar(
|
||||
scrollbarOrientation: mediaQuery.lgAndUp
|
||||
? ScrollbarOrientation.bottom
|
||||
: ScrollbarOrientation.top,
|
||||
controller: playlistController,
|
||||
child: Waypoint(
|
||||
onTouchEdge: () {
|
||||
searchPlaylist.fetchNext();
|
||||
},
|
||||
controller: playlistController,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: playlistController,
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
...playlists.mapIndexed(
|
||||
(i, playlist) {
|
||||
if (i == playlists.length - 1 &&
|
||||
searchPlaylist.hasNextPage) {
|
||||
return const ShimmerPlaybuttonCard(
|
||||
count: 1);
|
||||
}
|
||||
return PlaylistCard(playlist);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (searchPlaylist.isLoadingPage)
|
||||
const CircularProgressIndicator(),
|
||||
if (searchPlaylist.hasPageError)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
searchPlaylist.errors.lastOrNull?.toString() ?? "",
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (artists.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
context.l10n.artists,
|
||||
style: theme.textTheme.titleLarge!,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.mouse,
|
||||
},
|
||||
),
|
||||
child: Scrollbar(
|
||||
controller: artistController,
|
||||
child: Waypoint(
|
||||
controller: artistController,
|
||||
onTouchEdge: () {
|
||||
searchArtist.fetchNext();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: artistController,
|
||||
child: Row(
|
||||
children: [
|
||||
...artists.mapIndexed(
|
||||
(i, artist) {
|
||||
if (i == artists.length - 1 &&
|
||||
searchArtist.hasNextPage) {
|
||||
return const ShimmerPlaybuttonCard(
|
||||
count: 1);
|
||||
}
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 15),
|
||||
child: ArtistCard(artist),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (searchArtist.isLoadingPage)
|
||||
const CircularProgressIndicator(),
|
||||
if (searchArtist.hasPageError)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
searchArtist.errors.lastOrNull?.toString() ?? "",
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (albums.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
context.l10n.albums,
|
||||
style: theme.textTheme.titleLarge!,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.mouse,
|
||||
},
|
||||
),
|
||||
child: Scrollbar(
|
||||
controller: albumController,
|
||||
child: Waypoint(
|
||||
controller: albumController,
|
||||
onTouchEdge: () {
|
||||
searchAlbum.fetchNext();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: albumController,
|
||||
child: Row(
|
||||
children: [
|
||||
...albums.mapIndexed((i, album) {
|
||||
if (i == albums.length - 1 &&
|
||||
searchAlbum.hasNextPage) {
|
||||
return const ShimmerPlaybuttonCard(count: 1);
|
||||
}
|
||||
return AlbumCard(
|
||||
TypeConversionUtils.simpleAlbum_X_Album(
|
||||
album,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (searchAlbum.isLoadingPage)
|
||||
const CircularProgressIndicator(),
|
||||
if (searchAlbum.hasPageError)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
searchAlbum.errors.lastOrNull?.toString() ?? "",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: Scaffold(
|
||||
@ -77,7 +359,7 @@ class SearchPage extends HookConsumerWidget {
|
||||
),
|
||||
color: theme.scaffoldBackgroundColor,
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
autofocus: queries.none((s) => s.hasPageData),
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(SpotubeIcons.search),
|
||||
hintText: "${context.l10n.search}...",
|
||||
@ -93,283 +375,64 @@ class SearchPage extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
HookBuilder(
|
||||
builder: (context) {
|
||||
final playlist =
|
||||
ref.watch(ProxyPlaylistNotifier.provider);
|
||||
final playlistNotifier =
|
||||
ref.watch(ProxyPlaylistNotifier.notifier);
|
||||
List<AlbumSimple> albums = [];
|
||||
List<Artist> artists = [];
|
||||
List<Track> tracks = [];
|
||||
List<PlaylistSimple> playlists = [];
|
||||
final pages = [
|
||||
...searchTrack.pages,
|
||||
...searchAlbum.pages,
|
||||
...searchPlaylist.pages,
|
||||
...searchArtist.pages,
|
||||
].expand<Page>((page) => page).toList();
|
||||
for (MapEntry<int, Page> page in pages.asMap().entries) {
|
||||
for (var item in page.value.items ?? []) {
|
||||
if (item is AlbumSimple) {
|
||||
albums.add(item);
|
||||
} else if (item is PlaylistSimple) {
|
||||
playlists.add(item);
|
||||
} else if (item is Artist) {
|
||||
artists.add(item);
|
||||
} else if (item is Track) {
|
||||
tracks.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 20,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (tracks.isNotEmpty)
|
||||
Text(
|
||||
context.l10n.songs,
|
||||
style: theme.textTheme.titleLarge!,
|
||||
),
|
||||
if (searchTrack.isLoadingPage)
|
||||
const CircularProgressIndicator()
|
||||
else if (searchTrack.hasPageError)
|
||||
Text(
|
||||
searchTrack.errors.lastOrNull
|
||||
?.toString() ??
|
||||
"",
|
||||
)
|
||||
else
|
||||
...tracks.mapIndexed((i, track) {
|
||||
return TrackTile(
|
||||
index: i,
|
||||
track: track,
|
||||
onTap: () async {
|
||||
final isTrackPlaying =
|
||||
playlist.activeTrack?.id ==
|
||||
track.id;
|
||||
if (!isTrackPlaying &&
|
||||
context.mounted) {
|
||||
final shouldPlay =
|
||||
(playlist.tracks.length) > 20
|
||||
? await showPromptDialog(
|
||||
context: context,
|
||||
title: context.l10n
|
||||
.playing_track(
|
||||
track.name!,
|
||||
),
|
||||
message: context.l10n
|
||||
.queue_clear_alert(
|
||||
playlist
|
||||
.tracks.length,
|
||||
),
|
||||
)
|
||||
: true;
|
||||
|
||||
if (shouldPlay) {
|
||||
await playlistNotifier.load(
|
||||
[track],
|
||||
autoPlay: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
if (searchTrack.hasNextPage &&
|
||||
tracks.isNotEmpty)
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: searchTrack.isRefreshingPage
|
||||
? null
|
||||
: () => searchTrack.fetchNext(),
|
||||
child: searchTrack.isRefreshingPage
|
||||
? const CircularProgressIndicator()
|
||||
: Text(context.l10n.load_more),
|
||||
),
|
||||
),
|
||||
if (playlists.isNotEmpty)
|
||||
Text(
|
||||
context.l10n.playlists,
|
||||
style: theme.textTheme.titleLarge!,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context)
|
||||
.copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.mouse,
|
||||
},
|
||||
),
|
||||
child: Scrollbar(
|
||||
scrollbarOrientation: mediaQuery.lgAndUp
|
||||
? ScrollbarOrientation.bottom
|
||||
: ScrollbarOrientation.top,
|
||||
controller: playlistController,
|
||||
child: Waypoint(
|
||||
onTouchEdge: () {
|
||||
searchPlaylist.fetchNext();
|
||||
},
|
||||
controller: playlistController,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: playlistController,
|
||||
child: Row(
|
||||
children: [
|
||||
...playlists.mapIndexed(
|
||||
(i, playlist) {
|
||||
if (i ==
|
||||
playlists.length -
|
||||
1 &&
|
||||
searchPlaylist
|
||||
.hasNextPage) {
|
||||
return const ShimmerPlaybuttonCard(
|
||||
count: 1);
|
||||
}
|
||||
return PlaylistCard(playlist);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: searchTerm.isEmpty
|
||||
? Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: mediaQuery.size.height * 0.2,
|
||||
),
|
||||
Icon(
|
||||
SpotubeIcons.web,
|
||||
size: 120,
|
||||
color: theme.colorScheme.onBackground
|
||||
.withOpacity(0.7),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
context.l10n.search_to_get_results,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: theme.colorScheme.onBackground
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: isFetching
|
||||
? Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: mediaQuery.lgAndUp
|
||||
? mediaQuery.size.width * 0.5
|
||||
: mediaQuery.size.width,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.crunching_results,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: theme.colorScheme.onBackground
|
||||
.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const LinearProgressIndicator(),
|
||||
],
|
||||
),
|
||||
if (searchPlaylist.isLoadingPage)
|
||||
const CircularProgressIndicator(),
|
||||
if (searchPlaylist.hasPageError)
|
||||
Text(
|
||||
searchPlaylist.errors.lastOrNull
|
||||
?.toString() ??
|
||||
"",
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (artists.isNotEmpty)
|
||||
Text(
|
||||
context.l10n.artists,
|
||||
style: theme.textTheme.titleLarge!,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context)
|
||||
.copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.mouse,
|
||||
},
|
||||
),
|
||||
child: Scrollbar(
|
||||
controller: artistController,
|
||||
child: Waypoint(
|
||||
controller: artistController,
|
||||
onTouchEdge: () {
|
||||
searchArtist.fetchNext();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: artistController,
|
||||
child: Row(
|
||||
children: [
|
||||
...artists.mapIndexed(
|
||||
(i, artist) {
|
||||
if (i == artists.length - 1 &&
|
||||
searchArtist
|
||||
.hasNextPage) {
|
||||
return const ShimmerPlaybuttonCard(
|
||||
count: 1);
|
||||
}
|
||||
return Container(
|
||||
margin: const EdgeInsets
|
||||
.symmetric(
|
||||
horizontal: 15),
|
||||
child: ArtistCard(artist),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (searchArtist.isLoadingPage)
|
||||
const CircularProgressIndicator(),
|
||||
if (searchArtist.hasPageError)
|
||||
Text(
|
||||
searchArtist.errors.lastOrNull
|
||||
?.toString() ??
|
||||
"",
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (albums.isNotEmpty)
|
||||
Text(
|
||||
context.l10n.albums,
|
||||
style: theme.textTheme.titleMedium!,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context)
|
||||
.copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.mouse,
|
||||
},
|
||||
),
|
||||
child: Scrollbar(
|
||||
controller: albumController,
|
||||
child: Waypoint(
|
||||
controller: albumController,
|
||||
onTouchEdge: () {
|
||||
searchAlbum.fetchNext();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: albumController,
|
||||
child: Row(
|
||||
children: [
|
||||
...albums.mapIndexed((i, album) {
|
||||
if (i == albums.length - 1 &&
|
||||
searchAlbum.hasNextPage) {
|
||||
return const ShimmerPlaybuttonCard(
|
||||
count: 1);
|
||||
}
|
||||
return AlbumCard(
|
||||
TypeConversionUtils
|
||||
.simpleAlbum_X_Album(
|
||||
album,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (searchAlbum.isLoadingPage)
|
||||
const CircularProgressIndicator(),
|
||||
if (searchAlbum.hasPageError)
|
||||
Text(
|
||||
searchAlbum.errors.lastOrNull
|
||||
?.toString() ??
|
||||
"",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
)
|
||||
: resultWidget,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
@ -11,7 +11,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:piped_client/piped_client.dart';
|
||||
import 'package:spotube/collections/env.dart';
|
||||
import 'package:spotube/collections/language_codes.dart';
|
||||
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/settings/color_scheme_picker_dialog.dart';
|
||||
import 'package:spotube/components/settings/section_card_with_heading.dart';
|
||||
@ -47,8 +46,8 @@ class SettingsPage extends HookConsumerWidget {
|
||||
}, []);
|
||||
|
||||
final pickDownloadLocation = useCallback(() async {
|
||||
final dirStr = await FilePicker.platform.getDirectoryPath(
|
||||
dialogTitle: context.l10n.download_location,
|
||||
final dirStr = await getDirectoryPath(
|
||||
initialDirectory: preferences.downloadLocation,
|
||||
);
|
||||
if (dirStr == null) return;
|
||||
preferences.setDownloadLocation(dirStr);
|
||||
|
||||
46
lib/services/cli/cli.dart
Normal file
46
lib/services/cli/cli.dart
Normal file
@ -0,0 +1,46 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:spotube/models/logger.dart';
|
||||
|
||||
Future<ArgResults> startCLI(List<String> args) async {
|
||||
final parser = ArgParser();
|
||||
|
||||
parser.addFlag(
|
||||
'verbose',
|
||||
abbr: 'v',
|
||||
help: 'Verbose mode',
|
||||
defaultsTo: !kReleaseMode,
|
||||
callback: (verbose) {
|
||||
if (verbose) {
|
||||
logEnv['VERBOSE'] = 'true';
|
||||
logEnv['DEBUG'] = 'true';
|
||||
logEnv['ERROR'] = 'true';
|
||||
}
|
||||
},
|
||||
);
|
||||
parser.addFlag(
|
||||
"version",
|
||||
help: "Print version and exit",
|
||||
negatable: false,
|
||||
);
|
||||
|
||||
parser.addFlag("help", abbr: "h", negatable: false);
|
||||
|
||||
final arguments = parser.parse(args);
|
||||
|
||||
if (arguments["help"] == true) {
|
||||
print(parser.usage);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (arguments["version"] == true) {
|
||||
final package = await PackageInfo.fromPlatform();
|
||||
print("Spotube v${package.version}");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
@ -1,7 +1,17 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/hooks/use_spotify_mutation.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
|
||||
typedef PlaylistCRUDVariables = ({
|
||||
String playlistName,
|
||||
bool? public,
|
||||
bool? collaborative,
|
||||
String? description,
|
||||
String? base64Image,
|
||||
});
|
||||
|
||||
class PlaylistMutations {
|
||||
const PlaylistMutations();
|
||||
@ -11,8 +21,8 @@ class PlaylistMutations {
|
||||
String playlistId, {
|
||||
List<String>? refreshQueries,
|
||||
List<String>? refreshInfiniteQueries,
|
||||
ValueChanged<bool>? onData,
|
||||
}) {
|
||||
final queryClient = useQueryClient();
|
||||
return useSpotifyMutation<bool, dynamic, bool, dynamic>(
|
||||
"toggle-playlist-like/$playlistId",
|
||||
(isLiked, spotify) async {
|
||||
@ -25,10 +35,12 @@ class PlaylistMutations {
|
||||
},
|
||||
ref: ref,
|
||||
refreshQueries: refreshQueries,
|
||||
refreshInfiniteQueries: refreshInfiniteQueries,
|
||||
onData: (data, recoveryData) async {
|
||||
await queryClient
|
||||
.refreshInfiniteQueryAllPages("current-user-playlists");
|
||||
refreshInfiniteQueries: [
|
||||
...?refreshInfiniteQueries,
|
||||
"current-user-playlists",
|
||||
],
|
||||
onData: (data, recoveryData) {
|
||||
onData?.call(data);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -47,4 +59,91 @@ class PlaylistMutations {
|
||||
refreshQueries: ["playlist-tracks/$playlistId"],
|
||||
);
|
||||
}
|
||||
|
||||
Mutation<Playlist, dynamic, PlaylistCRUDVariables> create(
|
||||
WidgetRef ref, {
|
||||
List<String>? trackIds,
|
||||
ValueChanged<dynamic>? onError,
|
||||
ValueChanged<Playlist>? onData,
|
||||
}) {
|
||||
final me = useQueries.user.me(ref);
|
||||
return useSpotifyMutation<Playlist, dynamic, PlaylistCRUDVariables, void>(
|
||||
"create-playlist",
|
||||
(variable, spotify) async {
|
||||
final playlist = await spotify.playlists.createPlaylist(
|
||||
me.data!.id!,
|
||||
variable.playlistName,
|
||||
collaborative: variable.collaborative,
|
||||
description: variable.description,
|
||||
public: variable.public,
|
||||
);
|
||||
|
||||
if (variable.base64Image != null) {
|
||||
await spotify.playlists.updatePlaylistImage(
|
||||
playlist.id!,
|
||||
variable.base64Image!,
|
||||
);
|
||||
}
|
||||
|
||||
if (trackIds != null && trackIds.isNotEmpty) {
|
||||
await spotify.playlists.addTracks(
|
||||
trackIds.map((id) => "spotify:track:$id").toList(),
|
||||
playlist.id!,
|
||||
);
|
||||
}
|
||||
|
||||
return playlist;
|
||||
},
|
||||
refreshInfiniteQueries: [
|
||||
"current-user-playlists",
|
||||
],
|
||||
ref: ref,
|
||||
onError: (error, recoveryData) {
|
||||
onError?.call(error);
|
||||
},
|
||||
onData: (data, recoveryData) {
|
||||
onData?.call(data);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Mutation<void, dynamic, PlaylistCRUDVariables> update(
|
||||
WidgetRef ref, {
|
||||
String? playlistId,
|
||||
ValueChanged<dynamic>? onError,
|
||||
ValueChanged<void>? onData,
|
||||
}) {
|
||||
return useSpotifyMutation<void, dynamic, PlaylistCRUDVariables, void>(
|
||||
"update-playlist/$playlistId",
|
||||
(variable, spotify) async {
|
||||
if (playlistId == null) return;
|
||||
await spotify.playlists.updatePlaylist(
|
||||
playlistId,
|
||||
variable.playlistName,
|
||||
collaborative: variable.collaborative,
|
||||
description: variable.description,
|
||||
public: variable.public,
|
||||
);
|
||||
if (variable.base64Image != null) {
|
||||
await spotify.playlists.updatePlaylistImage(
|
||||
playlistId,
|
||||
variable.base64Image!,
|
||||
);
|
||||
}
|
||||
},
|
||||
refreshQueries: [
|
||||
"playlist/$playlistId",
|
||||
],
|
||||
refreshInfiniteQueries: [
|
||||
"current-user-playlists",
|
||||
],
|
||||
ref: ref,
|
||||
onError: (error, recoveryData) {
|
||||
onError?.call(error);
|
||||
},
|
||||
onData: (data, recoveryData) {
|
||||
onData?.call(data);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:catcher/catcher.dart';
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
@ -10,6 +13,7 @@ import 'package:spotube/extensions/track.dart';
|
||||
import 'package:spotube/hooks/use_spotify_infinite_query.dart';
|
||||
import 'package:spotube/hooks/use_spotify_query.dart';
|
||||
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/custom_spotify_endpoint_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||
|
||||
@ -142,14 +146,49 @@ class PlaylistQueries {
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Track>> tracksOf(String playlistId, SpotifyApi spotify) {
|
||||
if (playlistId == "user-liked-tracks") {
|
||||
return spotify.tracks.me.saved.all().then(
|
||||
(tracks) => tracks.map((e) => e.track!).toList(),
|
||||
);
|
||||
}
|
||||
Future<List<Track>> likedTracks(
|
||||
SpotifyApi spotify,
|
||||
WidgetRef ref,
|
||||
) async {
|
||||
final tracks = await spotify.tracks.me.saved.all();
|
||||
|
||||
return tracks.map((e) => e.track!).toList();
|
||||
}
|
||||
|
||||
Query<List<Track>, dynamic> likedTracksQuery(WidgetRef ref) {
|
||||
final query = useCallback((spotify) => likedTracks(spotify, ref), []);
|
||||
final context = useContext();
|
||||
|
||||
return useSpotifyQuery<List<Track>, dynamic>(
|
||||
"user-liked-tracks",
|
||||
query,
|
||||
jsonConfig: JsonConfig(
|
||||
toJson: (tracks) => <String, dynamic>{
|
||||
'tracks': tracks.map((e) => e.toJson()).toList(),
|
||||
},
|
||||
fromJson: (json) => (json['tracks'] as List)
|
||||
.map(
|
||||
(e) => Track.fromJson((e as Map).castKeyDeep<String>()),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
refreshConfig: RefreshConfig.withDefaults(
|
||||
context,
|
||||
// will never make it stale
|
||||
staleDuration: const Duration(days: 60),
|
||||
),
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Track>> tracksOf(
|
||||
String playlistId,
|
||||
SpotifyApi spotify,
|
||||
WidgetRef ref,
|
||||
) async {
|
||||
if (playlistId == "user-liked-tracks") return <Track>[];
|
||||
return spotify.playlists.getTracksByPlaylistId(playlistId).all().then(
|
||||
(value) => value.toList(),
|
||||
(value) => value.where((track) => track.id != null).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -159,19 +198,17 @@ class PlaylistQueries {
|
||||
) {
|
||||
return useSpotifyQuery<List<Track>, dynamic>(
|
||||
"playlist-tracks/$playlistId",
|
||||
(spotify) => tracksOf(playlistId, spotify),
|
||||
jsonConfig: playlistId == "user-liked-tracks"
|
||||
? JsonConfig(
|
||||
toJson: (tracks) => <String, dynamic>{
|
||||
'tracks': tracks.map((e) => e.toJson()).toList()
|
||||
},
|
||||
fromJson: (json) => (json['tracks'] as List)
|
||||
.map((e) => Track.fromJson(
|
||||
(e as Map).castKeyDeep<String>(),
|
||||
))
|
||||
.toList(),
|
||||
)
|
||||
: null,
|
||||
(spotify) => tracksOf(playlistId, spotify, ref),
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
Query<Playlist, dynamic> byId(WidgetRef ref, String id) {
|
||||
return useSpotifyQuery<Playlist, dynamic>(
|
||||
"playlist/$id",
|
||||
(spotify) async {
|
||||
return await spotify.playlists.get(id);
|
||||
},
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/hooks/use_spotify_query.dart';
|
||||
@ -8,6 +9,8 @@ import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
class UserQueries {
|
||||
const UserQueries();
|
||||
Query<User?, dynamic> me(WidgetRef ref) {
|
||||
final context = useContext();
|
||||
|
||||
return useSpotifyQuery<User, dynamic>(
|
||||
"current-user",
|
||||
(spotify) async {
|
||||
@ -26,6 +29,11 @@ class UserQueries {
|
||||
}
|
||||
return me;
|
||||
},
|
||||
refreshConfig: RefreshConfig.withDefaults(
|
||||
context,
|
||||
// will never make it stale
|
||||
staleDuration: const Duration(days: 60),
|
||||
),
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
@ -51,6 +51,7 @@ ThemeData theme(Color seed, Brightness brightness) {
|
||||
sliderTheme: SliderThemeData(overlayShape: SliderComponentShape.noOverlay),
|
||||
searchBarTheme: SearchBarThemeData(
|
||||
constraints: const BoxConstraints(maxWidth: double.infinity),
|
||||
padding: const MaterialStatePropertyAll(EdgeInsets.all(8)),
|
||||
backgroundColor: MaterialStatePropertyAll(
|
||||
Color.lerp(
|
||||
scheme.surfaceVariant,
|
||||
|
||||
@ -31,17 +31,6 @@ abstract class PrimitiveUtils {
|
||||
}
|
||||
}
|
||||
|
||||
static String zeroPadNumStr(int input) {
|
||||
return input < 10 ? "0$input" : input.toString();
|
||||
}
|
||||
|
||||
static String toReadableDuration(Duration duration) {
|
||||
final hours = duration.inHours;
|
||||
final minutes = duration.inMinutes % 60;
|
||||
final seconds = duration.inSeconds % 60;
|
||||
return "${hours > 0 ? "${zeroPadNumStr(hours)}:" : ""}${zeroPadNumStr(minutes)}:${zeroPadNumStr(seconds)}";
|
||||
}
|
||||
|
||||
static Future<T> raceMultiple<T>(
|
||||
Future<T> Function() inner, {
|
||||
Duration timeout = const Duration(milliseconds: 2500),
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <catcher/catcher_plugin.h>
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
#include <local_notifier/local_notifier_plugin.h>
|
||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||
@ -21,6 +22,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) catcher_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "CatcherPlugin");
|
||||
catcher_plugin_register_with_registrar(catcher_registrar);
|
||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
catcher
|
||||
file_selector_linux
|
||||
flutter_secure_storage_linux
|
||||
local_notifier
|
||||
media_kit_libs_linux
|
||||
|
||||
@ -16,6 +16,7 @@ dependencies:
|
||||
- libsecret-1-0
|
||||
- libnotify-bin
|
||||
- libjsoncpp25
|
||||
- libmpv2
|
||||
|
||||
essential: false
|
||||
icon: assets/spotube-logo.png
|
||||
|
||||
@ -9,6 +9,7 @@ import audio_service
|
||||
import audio_session
|
||||
import catcher
|
||||
import device_info_plus
|
||||
import file_selector_macos
|
||||
import flutter_secure_storage_macos
|
||||
import local_notifier
|
||||
import media_kit_libs_macos_audio
|
||||
@ -28,6 +29,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
|
||||
CatcherPlugin.register(with: registry.registrar(forPlugin: "CatcherPlugin"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
|
||||
MediaKitLibsMacosAudioPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosAudioPlugin"))
|
||||
|
||||
@ -19,5 +19,8 @@
|
||||
<!-- Requires Certification -->
|
||||
<!-- <key>keychain-access-groups</key>
|
||||
<array /> -->
|
||||
<!-- FilePicker -->
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true />
|
||||
</dict>
|
||||
</plist>
|
||||
@ -1,39 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>$(PRODUCT_COPYRIGHT)</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>NSAllowsArbitraryLoadsForMedia</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>$(PRODUCT_COPYRIGHT)</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true />
|
||||
<key>NSAllowsArbitraryLoadsForMedia</key>
|
||||
<true />
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -17,5 +17,8 @@
|
||||
<!-- Requires Certification -->
|
||||
<!-- <key>keychain-access-groups</key>
|
||||
<array /> -->
|
||||
<!-- FilePicker -->
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true />
|
||||
</dict>
|
||||
</plist>
|
||||
184
pubspec.lock
184
pubspec.lock
@ -338,6 +338,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: fd832b5384d0d6da4f6df60b854d33accaaeb63aa9e10e736a87381f08dee2cb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3+5"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -498,14 +506,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
file_picker:
|
||||
file_selector:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: c7a8e25ca60e7f331b153b0cb3d405828f18d3e72a6fa1d9440c86556fffc877
|
||||
name: file_selector
|
||||
sha256: "84eaf3e034d647859167d1f01cfe7b6352488f34c1b4932635012b202014c25b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.3.0"
|
||||
version: "1.0.1"
|
||||
file_selector_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_android
|
||||
sha256: d41e165d6f798ca941d536e5dc93494d50e78c571c28ad60cfe0b0fefeb9f1e7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0+3"
|
||||
file_selector_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_ios
|
||||
sha256: b3fbdda64aa2e335df6e111f6b0f1bb968402ed81d2dd1fa4274267999aa32c2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1+6"
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_linux
|
||||
sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.2+1"
|
||||
file_selector_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_macos
|
||||
sha256: "182c3f8350cee659f7b115e956047ee3dc672a96665883a545e81581b9a82c72"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+2"
|
||||
file_selector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_platform_interface
|
||||
sha256: "0aa47a725c346825a2bd396343ce63ac00bda6eff2fbc43eabe99737dede8262"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
file_selector_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_web
|
||||
sha256: dc6622c4d66cb1bee623ddcc029036603c6cc45c85e4a775bb06008d61c809c1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.2+1"
|
||||
file_selector_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_windows
|
||||
sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -518,18 +582,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fl_query
|
||||
sha256: "64f482fc09eb1166adca232f68772b2b11c616d88bce3208b2753c940ebc9f71"
|
||||
sha256: "3d71cd1eeb3232efa5e32363a351d74fd9ff07c6eb80aeb672b1970962764945"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0-alpha.3"
|
||||
version: "1.0.0-alpha.4"
|
||||
fl_query_devtools:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fl_query_devtools
|
||||
sha256: "72fac45293902b9f99c726609cd5416573566cce0b7c6e27311efde7fdf1b8b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0-alpha.2"
|
||||
fl_query_hooks:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fl_query_hooks
|
||||
sha256: b0ffc81fb047cbcedd9766776f9c72b95382730ce173226f0695c3f45774b0bc
|
||||
sha256: "7f0880696666714f77981777509a8aedb765857dcdbdde23e623da20a24c4ae0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0-alpha.3"
|
||||
version: "1.0.0-alpha.4+1"
|
||||
fluentui_system_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -694,10 +766,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_riverpod
|
||||
sha256: "0c997763ce06359ee4686553b74def84062e9d6929ac63f61fa02465c1f8e32c"
|
||||
sha256: "1bd39b04f1bcd217a969589777ca6bd642d116e3e5de65c3e6a8e8bdd8b178ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.4.0"
|
||||
flutter_rust_bridge:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -780,6 +852,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.3"
|
||||
form_validator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: form_validator
|
||||
sha256: "8cbe91b7d5260870d6fb9e23acd55d5d1d1fdf2397f0279a4931ac3c0c7bf8fb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
freezed_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -893,10 +973,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hooks_riverpod
|
||||
sha256: "71695b2e1dfc22a39f1f9c67b798f8f8f1521f2d0349817d13ccdd5c4cd7acba"
|
||||
sha256: ad7b877c3687e38764633d221a1f65491bc7a540e724101e9a404a84db2a4276
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.4.0"
|
||||
html:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -937,6 +1017,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.17"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: "7d7f2768df2a8b0a3cefa5ef4f84636121987d403130e70b17ef7e2cf650ba84"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: d32a997bcc4ee135aebca8e272b7c517927aa65a74b9c60a81a2764ef1a0462d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.7+5"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "50bc9ae6a77eea3a8b11af5eb6c661eeb858fdd2f734c2a4fd17086922347ef7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: c5538cacefacac733c724be7484377923b476216ad1ead35a0d2eadcdc0fc497
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.8+2"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_linux
|
||||
sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
image_picker_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_macos
|
||||
sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
sha256: ed9b00e63977c93b0d2d2b343685bed9c324534ba5abafbb3dfbd6a780b1b514
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.1"
|
||||
image_picker_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_windows
|
||||
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
integration_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@ -998,6 +1142,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.2"
|
||||
json_view:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_view
|
||||
sha256: "905c69f9e69d1eab5406b87ab6c10c3706c04c70c6a4959621bd2b43c2d27374"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
jwt_decode:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1474,10 +1626,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: riverpod
|
||||
sha256: "0f43c64f1f79c2112c843305a879a746587fb7c1e388f1d4717737796756e2c4"
|
||||
sha256: a600120d6f213a9922860eea1abc32597436edd5b2c4e73b91410f8c2af67d22
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.4.0"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1610,10 +1762,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: skeleton_text
|
||||
sha256: "6e088723b97ddcccfcce45312ce5e385ed1e5139a57afdf574f753d51eaa77f1"
|
||||
sha256: bacd536bf0664efe1cae53bcbd78c3d4040a120f300f69dc85d83f358471cc6c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.0.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
||||
12
pubspec.yaml
12
pubspec.yaml
@ -33,9 +33,9 @@ dependencies:
|
||||
disable_battery_optimization: ^1.1.0+1
|
||||
duration: ^3.0.12
|
||||
envied: ^0.3.0
|
||||
file_picker: ^5.2.2
|
||||
fl_query: ^1.0.0-alpha.3
|
||||
fl_query_hooks: ^1.0.0-alpha.3
|
||||
fl_query: ^1.0.0-alpha.4
|
||||
fl_query_hooks: ^1.0.0-alpha.4+1
|
||||
fl_query_devtools: ^0.1.0-alpha.2
|
||||
fluentui_system_icons: ^1.1.189
|
||||
flutter:
|
||||
sdk: flutter
|
||||
@ -54,6 +54,7 @@ dependencies:
|
||||
flutter_riverpod: ^2.1.1
|
||||
flutter_secure_storage: ^8.0.0
|
||||
flutter_svg: ^1.1.6
|
||||
form_validator: ^2.1.1
|
||||
fuzzywuzzy: ^0.2.0
|
||||
google_fonts: ^5.1.0
|
||||
go_router: ^10.0.0
|
||||
@ -81,7 +82,7 @@ dependencies:
|
||||
scroll_to_index: ^3.0.1
|
||||
shared_preferences: ^2.0.11
|
||||
sidebarx: ^0.15.0
|
||||
skeleton_text: ^3.0.0
|
||||
skeleton_text: ^3.0.1
|
||||
smtc_windows: ^0.1.0
|
||||
spotify: ^0.11.0
|
||||
supabase: ^1.9.9
|
||||
@ -99,6 +100,8 @@ dependencies:
|
||||
path: plugins/window_size
|
||||
youtube_explode_dart: ^2.0.1
|
||||
stroke_text: ^0.0.2
|
||||
image_picker: ^1.0.4
|
||||
file_selector: ^1.0.1
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.3.2
|
||||
@ -118,7 +121,6 @@ dev_dependencies:
|
||||
|
||||
dependency_overrides:
|
||||
http: ^1.1.0
|
||||
flutter_hooks: ^0.20.0
|
||||
|
||||
flutter:
|
||||
generate: true
|
||||
|
||||
@ -1 +1,78 @@
|
||||
{}
|
||||
{
|
||||
"bn": [
|
||||
"update_playlist",
|
||||
"update",
|
||||
"crunching_results",
|
||||
"search_to_get_results"
|
||||
],
|
||||
|
||||
"ca": [
|
||||
"update_playlist",
|
||||
"update",
|
||||
"crunching_results",
|
||||
"search_to_get_results"
|
||||
],
|
||||
|
||||
"de": [
|
||||
"update_playlist",
|
||||
"update",
|
||||
"crunching_results",
|
||||
"search_to_get_results"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"update_playlist",
|
||||
"update",
|
||||
"crunching_results",
|
||||
"search_to_get_results"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"update_playlist",
|
||||
"update",
|
||||
"crunching_results",
|
||||
"search_to_get_results"
|
||||
],
|
||||
|
||||
"hi": [
|
||||
"update_playlist",
|
||||
"update",
|
||||
"crunching_results",
|
||||
"search_to_get_results"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
"update_playlist",
|
||||
"update",
|
||||
"crunching_results",
|
||||
"search_to_get_results"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
"update_playlist",
|
||||
"update",
|
||||
"crunching_results",
|
||||
"search_to_get_results"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
"update_playlist",
|
||||
"update",
|
||||
"crunching_results",
|
||||
"search_to_get_results"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
"update_playlist",
|
||||
"update",
|
||||
"crunching_results",
|
||||
"search_to_get_results"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
"update_playlist",
|
||||
"update",
|
||||
"crunching_results",
|
||||
"search_to_get_results"
|
||||
]
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <catcher/catcher_plugin.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <local_notifier/local_notifier_plugin.h>
|
||||
#include <media_kit_libs_windows_audio/media_kit_libs_windows_audio_plugin_c_api.h>
|
||||
@ -21,6 +22,8 @@
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
CatcherPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("CatcherPlugin"));
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
LocalNotifierPluginRegisterWithRegistrar(
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
catcher
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
local_notifier
|
||||
media_kit_libs_windows_audio
|
||||
|
||||
Loading…
Reference in New Issue
Block a user