mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-05-08 16:24:36 +00:00
feat: make control page adaptive
This commit is contained in:
parent
22cc210f30
commit
7e887d54ed
@ -283,12 +283,17 @@ class UserLocalTracks extends HookConsumerWidget {
|
|||||||
trackSnapshot.isLoading ? 5 : filteredTracks.length,
|
trackSnapshot.isLoading ? 5 : filteredTracks.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (trackSnapshot.isLoading) {
|
if (trackSnapshot.isLoading) {
|
||||||
return TrackTile(track: FakeData.track, index: index);
|
return TrackTile(
|
||||||
|
playlist: playlist,
|
||||||
|
track: FakeData.track,
|
||||||
|
index: index,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final track = filteredTracks[index];
|
final track = filteredTracks[index];
|
||||||
return TrackTile(
|
return TrackTile(
|
||||||
index: index,
|
index: index,
|
||||||
|
playlist: playlist,
|
||||||
track: track,
|
track: track,
|
||||||
userPlaylist: false,
|
userPlaylist: false,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
@ -311,8 +316,11 @@ class UserLocalTracks extends HookConsumerWidget {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: 5,
|
itemCount: 5,
|
||||||
itemBuilder: (context, index) =>
|
itemBuilder: (context, index) => TrackTile(
|
||||||
TrackTile(track: FakeData.track, index: index),
|
track: FakeData.track,
|
||||||
|
index: index,
|
||||||
|
playlist: playlist,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -66,7 +66,6 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
topRight: Radius.circular(10),
|
topRight: Radius.circular(10),
|
||||||
);
|
);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final mediaQuery = MediaQuery.of(context);
|
|
||||||
final headlineColor = theme.textTheme.headlineSmall?.color;
|
final headlineColor = theme.textTheme.headlineSmall?.color;
|
||||||
|
|
||||||
final filteredTracks = useMemoized(
|
final filteredTracks = useMemoized(
|
||||||
@ -105,201 +104,206 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
return const NotFound(vertical: true);
|
return const NotFound(vertical: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ClipRRect(
|
return LayoutBuilder(builder: (context, constrains) {
|
||||||
borderRadius: borderRadius,
|
return ClipRRect(
|
||||||
clipBehavior: Clip.hardEdge,
|
borderRadius: borderRadius,
|
||||||
child: BackdropFilter(
|
clipBehavior: Clip.hardEdge,
|
||||||
filter: ImageFilter.blur(
|
child: BackdropFilter(
|
||||||
sigmaX: 15,
|
filter: ImageFilter.blur(
|
||||||
sigmaY: 15,
|
sigmaX: 15,
|
||||||
),
|
sigmaY: 15,
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: 5.0,
|
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
padding: const EdgeInsets.only(
|
||||||
borderRadius: borderRadius,
|
top: 5.0,
|
||||||
),
|
),
|
||||||
child: CallbackShortcuts(
|
decoration: BoxDecoration(
|
||||||
bindings: {
|
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
||||||
LogicalKeySet(LogicalKeyboardKey.escape): () {
|
borderRadius: borderRadius,
|
||||||
if (!isSearching.value) {
|
),
|
||||||
Navigator.of(context).pop();
|
child: CallbackShortcuts(
|
||||||
|
bindings: {
|
||||||
|
LogicalKeySet(LogicalKeyboardKey.escape): () {
|
||||||
|
if (!isSearching.value) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
isSearching.value = false;
|
||||||
|
searchText.value = '';
|
||||||
}
|
}
|
||||||
isSearching.value = false;
|
},
|
||||||
searchText.value = '';
|
child: Column(
|
||||||
}
|
children: [
|
||||||
},
|
if (!floating)
|
||||||
child: Column(
|
Container(
|
||||||
children: [
|
height: 5,
|
||||||
if (!floating)
|
width: 100,
|
||||||
Container(
|
margin: const EdgeInsets.only(bottom: 5, top: 2),
|
||||||
height: 5,
|
decoration: BoxDecoration(
|
||||||
width: 100,
|
color: headlineColor,
|
||||||
margin: const EdgeInsets.only(bottom: 5, top: 2),
|
borderRadius: BorderRadius.circular(20),
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: headlineColor,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
if (mediaQuery.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(),
|
),
|
||||||
],
|
Row(
|
||||||
if (mediaQuery.mdAndUp || isSearching.value)
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
TextField(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
onChanged: (value) {
|
children: [
|
||||||
searchText.value = value;
|
if (constrains.mdAndUp || !isSearching.value) ...[
|
||||||
},
|
const SizedBox(width: 10),
|
||||||
decoration: InputDecoration(
|
Text(
|
||||||
hintText: context.l10n.search,
|
context.l10n.tracks_in_queue(tracks.length),
|
||||||
isDense: true,
|
style: TextStyle(
|
||||||
prefixIcon: mediaQuery.smAndDown
|
color: headlineColor,
|
||||||
? IconButton(
|
fontWeight: FontWeight.bold,
|
||||||
icon: const Icon(
|
fontSize: 18,
|
||||||
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: mediaQuery.smAndDown
|
|
||||||
? mediaQuery.size.width - 40
|
|
||||||
: 300,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
const Spacer(),
|
||||||
else
|
],
|
||||||
IconButton.filledTonal(
|
if (constrains.mdAndUp || isSearching.value)
|
||||||
icon: const Icon(SpotubeIcons.filter),
|
TextField(
|
||||||
onPressed: () {
|
onChanged: (value) {
|
||||||
isSearching.value = !isSearching.value;
|
searchText.value = value;
|
||||||
},
|
},
|
||||||
),
|
decoration: InputDecoration(
|
||||||
if (mediaQuery.mdAndUp || !isSearching.value) ...[
|
hintText: context.l10n.search,
|
||||||
const SizedBox(width: 10),
|
isDense: true,
|
||||||
FilledButton(
|
prefixIcon: constrains.smAndDown
|
||||||
style: FilledButton.styleFrom(
|
? IconButton(
|
||||||
backgroundColor:
|
icon: const Icon(
|
||||||
theme.scaffoldBackgroundColor.withOpacity(0.5),
|
Icons.arrow_back_ios_new_outlined,
|
||||||
foregroundColor: theme.textTheme.headlineSmall?.color,
|
),
|
||||||
),
|
onPressed: () {
|
||||||
child: Row(
|
isSearching.value = false;
|
||||||
children: [
|
searchText.value = '';
|
||||||
const Icon(SpotubeIcons.playlistRemove),
|
},
|
||||||
const SizedBox(width: 5),
|
style: IconButton.styleFrom(
|
||||||
Text(context.l10n.clear_all),
|
padding: EdgeInsets.zero,
|
||||||
],
|
minimumSize: const Size.square(20),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
)
|
||||||
onStop();
|
: const Icon(SpotubeIcons.filter),
|
||||||
Navigator.of(context).pop();
|
constraints: BoxConstraints(
|
||||||
},
|
maxHeight: 40,
|
||||||
),
|
maxWidth: constrains.smAndDown
|
||||||
const SizedBox(width: 10),
|
? constrains.maxWidth - 40
|
||||||
],
|
: 300,
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
if (!isSearching.value && searchText.value.isEmpty)
|
|
||||||
Flexible(
|
|
||||||
child: ReorderableListView.builder(
|
|
||||||
onReorder: (oldIndex, newIndex) {
|
|
||||||
onReorder(oldIndex, newIndex);
|
|
||||||
},
|
|
||||||
scrollController: controller,
|
|
||||||
itemCount: tracks.length,
|
|
||||||
shrinkWrap: true,
|
|
||||||
buildDefaultDragHandles: false,
|
|
||||||
onReorderStart: (index) {
|
|
||||||
HapticFeedback.selectionClick();
|
|
||||||
},
|
|
||||||
onReorderEnd: (index) {
|
|
||||||
HapticFeedback.selectionClick();
|
|
||||||
},
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
onJump(track);
|
|
||||||
},
|
|
||||||
leadingActions: [
|
|
||||||
ReorderableDragStartListener(
|
|
||||||
index: i,
|
|
||||||
child: const Icon(SpotubeIcons.dragHandle),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
},
|
else
|
||||||
),
|
IconButton.filledTonal(
|
||||||
)
|
icon: const Icon(SpotubeIcons.filter),
|
||||||
else
|
onPressed: () {
|
||||||
Flexible(
|
isSearching.value = !isSearching.value;
|
||||||
child: InterScrollbar(
|
},
|
||||||
controller: controller,
|
),
|
||||||
child: ListView.builder(
|
if (constrains.mdAndUp || !isSearching.value) ...[
|
||||||
controller: controller,
|
const SizedBox(width: 10),
|
||||||
itemCount: filteredTracks.length,
|
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: () {
|
||||||
|
onStop();
|
||||||
|
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) {
|
||||||
|
onReorder(oldIndex, newIndex);
|
||||||
|
},
|
||||||
|
scrollController: controller,
|
||||||
|
itemCount: tracks.length,
|
||||||
|
shrinkWrap: true,
|
||||||
|
buildDefaultDragHandles: false,
|
||||||
|
onReorderStart: (index) {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
},
|
||||||
|
onReorderEnd: (index) {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
},
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
final track = filteredTracks.elementAt(i);
|
final track = tracks.elementAt(i);
|
||||||
return Padding(
|
return AutoScrollTag(
|
||||||
padding:
|
key: ValueKey(i),
|
||||||
const EdgeInsets.symmetric(horizontal: 8.0),
|
controller: controller,
|
||||||
child: TrackTile(
|
index: i,
|
||||||
index: i,
|
child: Padding(
|
||||||
track: track,
|
padding:
|
||||||
onTap: () async {
|
const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
if (playlist.activeTrack?.id == track.id) {
|
child: TrackTile(
|
||||||
return;
|
index: i,
|
||||||
}
|
track: track,
|
||||||
onJump(track);
|
playlist: playlist,
|
||||||
},
|
onTap: () async {
|
||||||
|
if (playlist.activeTrack?.id == track.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onJump(track);
|
||||||
|
},
|
||||||
|
leadingActions: [
|
||||||
|
ReorderableDragStartListener(
|
||||||
|
index: i,
|
||||||
|
child: const Icon(SpotubeIcons.dragHandle),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Flexible(
|
||||||
|
child: InterScrollbar(
|
||||||
|
controller: controller,
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: controller,
|
||||||
|
itemCount: filteredTracks.length,
|
||||||
|
itemBuilder: (context, i) {
|
||||||
|
final track = filteredTracks.elementAt(i);
|
||||||
|
return Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: TrackTile(
|
||||||
|
index: i,
|
||||||
|
playlist: playlist,
|
||||||
|
track: track,
|
||||||
|
onTap: () async {
|
||||||
|
if (playlist.activeTrack?.id == track.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onJump(track);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import 'package:spotube/extensions/duration.dart';
|
|||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||||
|
|
||||||
class TrackTile extends HookConsumerWidget {
|
class TrackTile extends HookConsumerWidget {
|
||||||
/// [index] will not be shown if null
|
/// [index] will not be shown if null
|
||||||
@ -30,6 +30,7 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
final VoidCallback? onLongPress;
|
final VoidCallback? onLongPress;
|
||||||
final bool userPlaylist;
|
final bool userPlaylist;
|
||||||
final String? playlistId;
|
final String? playlistId;
|
||||||
|
final ProxyPlaylist playlist;
|
||||||
|
|
||||||
final List<Widget>? leadingActions;
|
final List<Widget>? leadingActions;
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
this.index,
|
this.index,
|
||||||
required this.track,
|
required this.track,
|
||||||
this.selected = false,
|
this.selected = false,
|
||||||
|
required this.playlist,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
@ -48,7 +50,6 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
final blacklist = ref.watch(BlackListNotifier.provider);
|
final blacklist = ref.watch(BlackListNotifier.provider);
|
||||||
@ -65,10 +66,10 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
|
|
||||||
final showOptionCbRef = useRef<ValueChanged<RelativeRect>?>(null);
|
final showOptionCbRef = useRef<ValueChanged<RelativeRect>?>(null);
|
||||||
|
|
||||||
final isPlaying = track.id == playlist.activeTrack?.id;
|
|
||||||
|
|
||||||
final isLoading = useState(false);
|
final isLoading = useState(false);
|
||||||
|
|
||||||
|
final isPlaying = playlist.activeTrack?.id == track.id;
|
||||||
|
|
||||||
final isSelected = isPlaying || isLoading.value;
|
final isSelected = isPlaying || isLoading.value;
|
||||||
|
|
||||||
return LayoutBuilder(builder: (context, constrains) {
|
return LayoutBuilder(builder: (context, constrains) {
|
||||||
|
|||||||
@ -89,6 +89,7 @@ class TrackViewBodySection extends HookConsumerWidget {
|
|||||||
loadingBuilder: (context) => Skeletonizer(
|
loadingBuilder: (context) => Skeletonizer(
|
||||||
enabled: true,
|
enabled: true,
|
||||||
child: TrackTile(
|
child: TrackTile(
|
||||||
|
playlist: playlist,
|
||||||
track: FakeData.track,
|
track: FakeData.track,
|
||||||
index: 0,
|
index: 0,
|
||||||
),
|
),
|
||||||
@ -98,13 +99,18 @@ class TrackViewBodySection extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: List.generate(
|
children: List.generate(
|
||||||
10,
|
10,
|
||||||
(index) => TrackTile(track: FakeData.track, index: index),
|
(index) => TrackTile(
|
||||||
|
track: FakeData.track,
|
||||||
|
index: index,
|
||||||
|
playlist: playlist,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final track = tracks[index];
|
final track = tracks[index];
|
||||||
return TrackTile(
|
return TrackTile(
|
||||||
|
playlist: playlist,
|
||||||
track: track,
|
track: track,
|
||||||
index: index,
|
index: index,
|
||||||
selected: trackViewState.selectedTrackIds.contains(track.id!),
|
selected: trackViewState.selectedTrackIds.contains(track.id!),
|
||||||
|
|||||||
@ -107,6 +107,7 @@ class ArtistPageTopTracks extends HookConsumerWidget {
|
|||||||
final track = topTracks.elementAt(index);
|
final track = topTracks.elementAt(index);
|
||||||
return TrackTile(
|
return TrackTile(
|
||||||
index: index,
|
index: index,
|
||||||
|
playlist: playlist,
|
||||||
track: track,
|
track: track,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
playPlaylist(
|
playPlaylist(
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@ -9,6 +8,7 @@ import 'package:spotube/components/shared/image/universal_image.dart';
|
|||||||
import 'package:spotube/components/shared/links/anchor_button.dart';
|
import 'package:spotube/components/shared/links/anchor_button.dart';
|
||||||
import 'package:spotube/components/shared/links/artist_link.dart';
|
import 'package:spotube/components/shared/links/artist_link.dart';
|
||||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||||
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/duration.dart';
|
import 'package:spotube/extensions/duration.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
@ -49,220 +49,250 @@ class ConnectControlPage extends HookConsumerWidget {
|
|||||||
minimumSize: const Size(28, 28),
|
minimumSize: const Size(28, 28),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final playerQueue = Consumer(builder: (context, ref, _) {
|
||||||
|
final playlist = ref.watch(queueProvider);
|
||||||
|
return PlayerQueue(
|
||||||
|
playlist: playlist,
|
||||||
|
floating: true,
|
||||||
|
onJump: (track) async {
|
||||||
|
final index = playlist.tracks.toList().indexOf(track);
|
||||||
|
connectNotifier.jumpTo(index);
|
||||||
|
},
|
||||||
|
onRemove: (track) async {
|
||||||
|
await connectNotifier.removeTrack(track);
|
||||||
|
},
|
||||||
|
onStop: () async => connectNotifier.stop(),
|
||||||
|
onReorder: (oldIndex, newIndex) async {
|
||||||
|
await connectNotifier.reorder(
|
||||||
|
(oldIndex: oldIndex, newIndex: newIndex),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
ref.listen(connectClientsProvider, (prev, next) {
|
ref.listen(connectClientsProvider, (prev, next) {
|
||||||
if (next.asData?.value.resolvedService == null) {
|
if (next.asData?.value.resolvedService == null) {
|
||||||
context.pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Scaffold(
|
return SafeArea(
|
||||||
appBar: PageWindowTitleBar(
|
child: Scaffold(
|
||||||
title: Text(resolvedService!.name),
|
appBar: PageWindowTitleBar(
|
||||||
automaticallyImplyLeading: true,
|
title: Text(resolvedService!.name),
|
||||||
),
|
automaticallyImplyLeading: true,
|
||||||
body: CustomScrollView(
|
),
|
||||||
slivers: [
|
body: LayoutBuilder(builder: (context, constrains) {
|
||||||
SliverToBoxAdapter(
|
return Row(
|
||||||
child: Padding(
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
Expanded(
|
||||||
child: ClipRRect(
|
child: CustomScrollView(
|
||||||
borderRadius: BorderRadius.circular(20),
|
slivers: [
|
||||||
child: UniversalImage(
|
SliverToBoxAdapter(
|
||||||
path: (playlist.activeTrack?.album?.images).asUrlString(
|
child: Container(
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
alignment: Alignment.center,
|
||||||
),
|
padding: const EdgeInsets.symmetric(
|
||||||
fit: BoxFit.cover,
|
horizontal: 20,
|
||||||
),
|
vertical: 10,
|
||||||
),
|
).copyWith(top: 0),
|
||||||
),
|
constraints:
|
||||||
),
|
const BoxConstraints(maxHeight: 400, maxWidth: 400),
|
||||||
const SliverGap(10),
|
child: ClipRRect(
|
||||||
SliverPadding(
|
borderRadius: BorderRadius.circular(20),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
child: UniversalImage(
|
||||||
sliver: SliverMainAxisGroup(
|
path: (playlist.activeTrack?.album?.images)
|
||||||
slivers: [
|
.asUrlString(
|
||||||
SliverToBoxAdapter(
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
child: AnchorButton(
|
),
|
||||||
playlist.activeTrack?.name ?? "",
|
fit: BoxFit.cover,
|
||||||
style: textTheme.titleLarge!,
|
),
|
||||||
onTap: () {
|
),
|
||||||
ServiceUtils.push(
|
|
||||||
context,
|
|
||||||
"/track/${playlist.activeTrack?.id}",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: ArtistLink(
|
|
||||||
artists: playlist.activeTrack?.artists ?? [],
|
|
||||||
textStyle: textTheme.bodyMedium!,
|
|
||||||
mainAxisAlignment: WrapAlignment.start,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SliverGap(30),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Consumer(
|
|
||||||
builder: (context, ref, _) {
|
|
||||||
final position = ref.watch(positionProvider);
|
|
||||||
final duration = ref.watch(durationProvider);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Slider(
|
|
||||||
value: position > duration
|
|
||||||
? 0
|
|
||||||
: position.inSeconds.toDouble(),
|
|
||||||
min: 0,
|
|
||||||
max: duration.inSeconds.toDouble(),
|
|
||||||
onChanged: (value) {
|
|
||||||
connectNotifier
|
|
||||||
.seek(Duration(seconds: value.toInt()));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
Row(
|
),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
SliverPadding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
Text(position.toHumanReadableString()),
|
sliver: SliverMainAxisGroup(
|
||||||
Text(duration.toHumanReadableString()),
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: AnchorButton(
|
||||||
|
playlist.activeTrack?.name ?? "",
|
||||||
|
style: textTheme.titleLarge!,
|
||||||
|
onTap: () {
|
||||||
|
ServiceUtils.push(
|
||||||
|
context,
|
||||||
|
"/track/${playlist.activeTrack?.id}",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: ArtistLink(
|
||||||
|
artists: playlist.activeTrack?.artists ?? [],
|
||||||
|
textStyle: textTheme.bodyMedium!,
|
||||||
|
mainAxisAlignment: WrapAlignment.start,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
const SliverGap(30),
|
||||||
);
|
SliverToBoxAdapter(
|
||||||
},
|
child: Consumer(
|
||||||
),
|
builder: (context, ref, _) {
|
||||||
),
|
final position = ref.watch(positionProvider);
|
||||||
SliverToBoxAdapter(
|
final duration = ref.watch(durationProvider);
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
return Padding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
IconButton(
|
child: Column(
|
||||||
tooltip: shuffled
|
children: [
|
||||||
? context.l10n.unshuffle_playlist
|
Slider(
|
||||||
: context.l10n.shuffle_playlist,
|
value: position > duration
|
||||||
icon: const Icon(SpotubeIcons.shuffle),
|
? 0
|
||||||
style: shuffled ? activeButtonStyle : buttonStyle,
|
: position.inSeconds.toDouble(),
|
||||||
onPressed: playlist.activeTrack == null
|
min: 0,
|
||||||
? null
|
max: duration.inSeconds.toDouble(),
|
||||||
: () {
|
onChanged: (value) {
|
||||||
connectNotifier.setShuffle(!shuffled);
|
connectNotifier
|
||||||
},
|
.seek(Duration(seconds: value.toInt()));
|
||||||
),
|
},
|
||||||
IconButton(
|
),
|
||||||
tooltip: context.l10n.previous_track,
|
Row(
|
||||||
icon: const Icon(SpotubeIcons.skipBack),
|
mainAxisAlignment:
|
||||||
onPressed: playlist.activeTrack == null
|
MainAxisAlignment.spaceBetween,
|
||||||
? null
|
children: [
|
||||||
: connectNotifier.previous,
|
Text(position.toHumanReadableString()),
|
||||||
),
|
Text(duration.toHumanReadableString()),
|
||||||
IconButton(
|
],
|
||||||
tooltip: playing
|
),
|
||||||
? context.l10n.pause_playback
|
],
|
||||||
: context.l10n.resume_playback,
|
),
|
||||||
icon: playlist.activeTrack == null
|
|
||||||
? SizedBox(
|
|
||||||
height: 20,
|
|
||||||
width: 20,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
color: colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Icon(
|
|
||||||
playing ? SpotubeIcons.pause : SpotubeIcons.play,
|
|
||||||
),
|
|
||||||
style: resumePauseStyle,
|
|
||||||
onPressed: playlist.activeTrack == null
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
if (playing) {
|
|
||||||
connectNotifier.pause();
|
|
||||||
} else {
|
|
||||||
connectNotifier.resume();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
tooltip: context.l10n.next_track,
|
|
||||||
icon: const Icon(SpotubeIcons.skipForward),
|
|
||||||
onPressed: playlist.activeTrack == null
|
|
||||||
? null
|
|
||||||
: connectNotifier.next,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
tooltip: loopMode == PlaybackLoopMode.one
|
|
||||||
? context.l10n.loop_track
|
|
||||||
: loopMode == PlaybackLoopMode.all
|
|
||||||
? context.l10n.repeat_playlist
|
|
||||||
: null,
|
|
||||||
icon: Icon(
|
|
||||||
loopMode == PlaybackLoopMode.one
|
|
||||||
? SpotubeIcons.repeatOne
|
|
||||||
: SpotubeIcons.repeat,
|
|
||||||
),
|
|
||||||
style: loopMode == PlaybackLoopMode.one ||
|
|
||||||
loopMode == PlaybackLoopMode.all
|
|
||||||
? activeButtonStyle
|
|
||||||
: buttonStyle,
|
|
||||||
onPressed: playlist.activeTrack == null
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
connectNotifier.setLoopMode(
|
|
||||||
switch (loopMode) {
|
|
||||||
PlaybackLoopMode.all => PlaybackLoopMode.one,
|
|
||||||
PlaybackLoopMode.one => PlaybackLoopMode.none,
|
|
||||||
PlaybackLoopMode.none => PlaybackLoopMode.all,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
],
|
),
|
||||||
),
|
SliverToBoxAdapter(
|
||||||
),
|
child: Row(
|
||||||
const SliverGap(30),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
SliverPadding(
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
IconButton(
|
||||||
sliver: SliverToBoxAdapter(
|
tooltip: shuffled
|
||||||
child: OutlinedButton.icon(
|
? context.l10n.unshuffle_playlist
|
||||||
icon: const Icon(SpotubeIcons.queue),
|
: context.l10n.shuffle_playlist,
|
||||||
label: Text(context.l10n.queue),
|
icon: const Icon(SpotubeIcons.shuffle),
|
||||||
onPressed: () {
|
style: shuffled ? activeButtonStyle : buttonStyle,
|
||||||
showModalBottomSheet(
|
onPressed: playlist.activeTrack == null
|
||||||
context: context,
|
? null
|
||||||
builder: (context) {
|
: () {
|
||||||
return Consumer(builder: (context, ref, _) {
|
connectNotifier.setShuffle(!shuffled);
|
||||||
final playlist = ref.watch(queueProvider);
|
},
|
||||||
return PlayerQueue(
|
),
|
||||||
playlist: playlist,
|
IconButton(
|
||||||
floating: true,
|
tooltip: context.l10n.previous_track,
|
||||||
onJump: (track) async {
|
icon: const Icon(SpotubeIcons.skipBack),
|
||||||
final index =
|
onPressed: playlist.activeTrack == null
|
||||||
playlist.tracks.toList().indexOf(track);
|
? null
|
||||||
connectNotifier.jumpTo(index);
|
: connectNotifier.previous,
|
||||||
},
|
),
|
||||||
onRemove: (track) async {
|
IconButton(
|
||||||
await connectNotifier.removeTrack(track);
|
tooltip: playing
|
||||||
},
|
? context.l10n.pause_playback
|
||||||
onStop: () async => connectNotifier.stop(),
|
: context.l10n.resume_playback,
|
||||||
onReorder: (oldIndex, newIndex) async {
|
icon: playlist.activeTrack == null
|
||||||
await connectNotifier.reorder(
|
? SizedBox(
|
||||||
(oldIndex: oldIndex, newIndex: newIndex),
|
height: 20,
|
||||||
);
|
width: 20,
|
||||||
},
|
child: CircularProgressIndicator(
|
||||||
);
|
color: colorScheme.onPrimary,
|
||||||
});
|
),
|
||||||
},
|
)
|
||||||
);
|
: Icon(
|
||||||
},
|
playing
|
||||||
|
? SpotubeIcons.pause
|
||||||
|
: SpotubeIcons.play,
|
||||||
|
),
|
||||||
|
style: resumePauseStyle,
|
||||||
|
onPressed: playlist.activeTrack == null
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
if (playing) {
|
||||||
|
connectNotifier.pause();
|
||||||
|
} else {
|
||||||
|
connectNotifier.resume();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: context.l10n.next_track,
|
||||||
|
icon: const Icon(SpotubeIcons.skipForward),
|
||||||
|
onPressed: playlist.activeTrack == null
|
||||||
|
? null
|
||||||
|
: connectNotifier.next,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: loopMode == PlaybackLoopMode.one
|
||||||
|
? context.l10n.loop_track
|
||||||
|
: loopMode == PlaybackLoopMode.all
|
||||||
|
? context.l10n.repeat_playlist
|
||||||
|
: null,
|
||||||
|
icon: Icon(
|
||||||
|
loopMode == PlaybackLoopMode.one
|
||||||
|
? SpotubeIcons.repeatOne
|
||||||
|
: SpotubeIcons.repeat,
|
||||||
|
),
|
||||||
|
style: loopMode == PlaybackLoopMode.one ||
|
||||||
|
loopMode == PlaybackLoopMode.all
|
||||||
|
? activeButtonStyle
|
||||||
|
: buttonStyle,
|
||||||
|
onPressed: playlist.activeTrack == null
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
connectNotifier.setLoopMode(
|
||||||
|
switch (loopMode) {
|
||||||
|
PlaybackLoopMode.all =>
|
||||||
|
PlaybackLoopMode.one,
|
||||||
|
PlaybackLoopMode.one =>
|
||||||
|
PlaybackLoopMode.none,
|
||||||
|
PlaybackLoopMode.none =>
|
||||||
|
PlaybackLoopMode.all,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SliverGap(30),
|
||||||
|
if (constrains.mdAndDown)
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
icon: const Icon(SpotubeIcons.queue),
|
||||||
|
label: Text(context.l10n.queue),
|
||||||
|
onPressed: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return playerQueue;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
if (constrains.lgAndUp) ...[
|
||||||
)
|
const VerticalDivider(thickness: 1),
|
||||||
],
|
Expanded(
|
||||||
|
child: playerQueue,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,7 +53,8 @@ class HomePage extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
|
const Gap(10),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const HomeGenresSection(),
|
const HomeGenresSection(),
|
||||||
|
|||||||
@ -46,6 +46,7 @@ class SearchTracksSection extends HookConsumerWidget {
|
|||||||
return TrackTile(
|
return TrackTile(
|
||||||
index: i,
|
index: i,
|
||||||
track: track,
|
track: track,
|
||||||
|
playlist: playlist,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final isTrackPlaying = playlist.activeTrack?.id == track.id;
|
final isTrackPlaying = playlist.activeTrack?.id == track.id;
|
||||||
if (!isTrackPlaying && context.mounted) {
|
if (!isTrackPlaying && context.mounted) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user