mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
refactor: remote playback page to shadcn
This commit is contained in:
parent
780f5dee2e
commit
af295be8c6
@ -1,3 +1,4 @@
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/components/button/back_button.dart';
|
||||
@ -60,6 +61,7 @@ class TitleBar extends HookConsumerWidget implements PreferredSizeWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final hasLeadingOrCanPop = leading.isNotEmpty || Navigator.canPop(context);
|
||||
final lastClicked = useRef<int>(DateTime.now().millisecondsSinceEpoch);
|
||||
|
||||
return SizedBox(
|
||||
height: height ?? 56,
|
||||
@ -71,6 +73,23 @@ class TitleBar extends HookConsumerWidget implements PreferredSizeWidget {
|
||||
return GestureDetector(
|
||||
onHorizontalDragStart: (_) => onDrag(ref),
|
||||
onVerticalDragStart: (_) => onDrag(ref),
|
||||
onTapDown: (details) async {
|
||||
final systemTitlebar = ref.read(
|
||||
userPreferencesProvider.select((s) => s.systemTitleBar));
|
||||
if (!kIsDesktop || systemTitlebar) return;
|
||||
|
||||
int currMills = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
if ((currMills - lastClicked.value) < 500) {
|
||||
if (await windowManager.isMaximized()) {
|
||||
await windowManager.unmaximize();
|
||||
} else {
|
||||
await windowManager.maximize();
|
||||
}
|
||||
} else {
|
||||
lastClicked.value = currMills;
|
||||
}
|
||||
},
|
||||
child: AppBar(
|
||||
leading: leading.isEmpty &&
|
||||
automaticallyImplyLeading &&
|
||||
|
95
lib/components/ui/button_tile.dart
Normal file
95
lib/components/ui/button_tile.dart
Normal file
@ -0,0 +1,95 @@
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
class ButtonTile extends StatelessWidget {
|
||||
final Widget? title;
|
||||
final Widget? subtitle;
|
||||
final Widget? leading;
|
||||
final Widget? trailing;
|
||||
final bool enabled;
|
||||
final void Function()? onPressed;
|
||||
final bool selected;
|
||||
final ButtonVariance style;
|
||||
|
||||
const ButtonTile({
|
||||
super.key,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.enabled = true,
|
||||
this.onPressed,
|
||||
this.selected = false,
|
||||
this.style = ButtonVariance.outline,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData(:colorScheme, :typography) = Theme.of(context);
|
||||
|
||||
return Button(
|
||||
enabled: enabled,
|
||||
onPressed: onPressed,
|
||||
style: style.copyWith(
|
||||
decoration: (context, states, value) {
|
||||
final decoration = ButtonVariance.outline.decoration(context, states)
|
||||
as BoxDecoration;
|
||||
|
||||
if (selected && style == ButtonVariance.outline) {
|
||||
return decoration.copyWith(
|
||||
border: Border.all(
|
||||
color: colorScheme.primary,
|
||||
width: 1.0,
|
||||
),
|
||||
color: colorScheme.primary.withAlpha(25),
|
||||
);
|
||||
}
|
||||
|
||||
return decoration;
|
||||
},
|
||||
iconTheme: (context, states, value) {
|
||||
final iconTheme = ButtonVariance.outline.iconTheme(context, states);
|
||||
|
||||
if (selected && style == ButtonVariance.outline) {
|
||||
return iconTheme.copyWith(
|
||||
color: colorScheme.primary,
|
||||
);
|
||||
}
|
||||
|
||||
return iconTheme;
|
||||
},
|
||||
textStyle: (context, states, value) {
|
||||
final textStyle = ButtonVariance.outline.textStyle(context, states);
|
||||
|
||||
if (selected && style == ButtonVariance.outline) {
|
||||
return textStyle.copyWith(
|
||||
color: colorScheme.primary,
|
||||
);
|
||||
}
|
||||
|
||||
return textStyle;
|
||||
},
|
||||
),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Basic(
|
||||
padding: EdgeInsets.zero,
|
||||
leadingAlignment: Alignment.center,
|
||||
trailingAlignment: Alignment.center,
|
||||
leading: leading,
|
||||
title: title,
|
||||
subtitle:
|
||||
style == ButtonVariance.outline && selected && subtitle != null
|
||||
? DefaultTextStyle(
|
||||
style: typography.xSmall.copyWith(
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
child: subtitle!,
|
||||
)
|
||||
: subtitle,
|
||||
trailing: trailing,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -97,6 +97,7 @@
|
||||
"pause_playback": "Pause Playback",
|
||||
"resume_playback": "Resume Playback",
|
||||
"loop_track": "Loop track",
|
||||
"no_loop": "No loop",
|
||||
"repeat_playlist": "Repeat playlist",
|
||||
"queue": "Queue",
|
||||
"alternative_track_sources": "Alternative track sources",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/ui/button_tile.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
|
||||
@ -10,7 +10,7 @@ class ConnectPageLocalDevices extends HookWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData(:textTheme) = Theme.of(context);
|
||||
final ThemeData(:typography) = Theme.of(context);
|
||||
final devicesFuture = useFuture(audioPlayer.devices);
|
||||
final devicesStream = useStream(audioPlayer.devicesStream);
|
||||
final selectedDeviceFuture = useFuture(audioPlayer.selectedDevice);
|
||||
@ -32,7 +32,7 @@ class ConnectPageLocalDevices extends HookWidget {
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text(
|
||||
context.l10n.this_device,
|
||||
style: textTheme.titleMedium,
|
||||
style: typography.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -43,14 +43,12 @@ class ConnectPageLocalDevices extends HookWidget {
|
||||
itemBuilder: (context, index) {
|
||||
final device = devices[index];
|
||||
|
||||
return Card(
|
||||
child: ListTile(
|
||||
return ButtonTile(
|
||||
selected: selectedDevice == device,
|
||||
onPressed: () => audioPlayer.setAudioDevice(device),
|
||||
leading: const Icon(SpotubeIcons.speaker),
|
||||
title: Text(device.description),
|
||||
subtitle: Text(device.name),
|
||||
selected: selectedDevice == device,
|
||||
onTap: () => audioPlayer.setAudioDevice(device),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -224,7 +224,7 @@ class PlayerQueue extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
const SliverGap(100),
|
||||
const SliverSafeArea(sliver: SliverGap(100)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/ui/button_tile.dart';
|
||||
import 'package:spotube/modules/connect/local_devices.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
@ -16,22 +16,19 @@ class ConnectPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ThemeData(:colorScheme, :textTheme) = Theme.of(context);
|
||||
final ThemeData(:colorScheme, :typography) = Theme.of(context);
|
||||
|
||||
final connectClients = ref.watch(connectClientsProvider);
|
||||
final connectClientsNotifier = ref.read(connectClientsProvider.notifier);
|
||||
final discoveredDevices = connectClients.asData?.value.services;
|
||||
|
||||
return Scaffold(
|
||||
appBar: TitleBar(
|
||||
headers: [
|
||||
TitleBar(
|
||||
automaticallyImplyLeading: true,
|
||||
title: Text(context.l10n.devices),
|
||||
),
|
||||
body: ListTileTheme(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
selectedTileColor: colorScheme.secondary.withOpacity(0.1),
|
||||
)
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: CustomScrollView(
|
||||
@ -41,7 +38,7 @@ class ConnectPage extends HookConsumerWidget {
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text(
|
||||
context.l10n.remote,
|
||||
style: textTheme.titleMedium,
|
||||
style: typography.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -54,8 +51,8 @@ class ConnectPage extends HookConsumerWidget {
|
||||
final selected =
|
||||
connectClients.asData?.value.resolvedService?.name ==
|
||||
device.name;
|
||||
return Card(
|
||||
child: ListTile(
|
||||
return ButtonTile(
|
||||
selected: selected,
|
||||
leading: const Icon(SpotubeIcons.monitor),
|
||||
title: Text(device.name),
|
||||
subtitle: selected
|
||||
@ -64,8 +61,15 @@ class ConnectPage extends HookConsumerWidget {
|
||||
":${connectClients.asData?.value.resolvedService?.port}",
|
||||
)
|
||||
: null,
|
||||
selected: selected,
|
||||
onTap: () {
|
||||
trailing: selected
|
||||
? IconButton.outline(
|
||||
icon: const Icon(SpotubeIcons.power),
|
||||
size: ButtonSize.small,
|
||||
onPressed: () =>
|
||||
connectClientsNotifier.clearResolvedService(),
|
||||
)
|
||||
: null,
|
||||
onPressed: () {
|
||||
if (selected) {
|
||||
ServiceUtils.pushNamed(
|
||||
context,
|
||||
@ -75,14 +79,6 @@ class ConnectPage extends HookConsumerWidget {
|
||||
connectClientsNotifier.resolveService(device);
|
||||
}
|
||||
},
|
||||
trailing: selected
|
||||
? IconButton(
|
||||
icon: const Icon(SpotubeIcons.power),
|
||||
onPressed: () =>
|
||||
connectClientsNotifier.clearResolvedService(),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -90,7 +86,6 @@ class ConnectPage extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/modules/player/player_queue.dart';
|
||||
import 'package:spotube/modules/player/volume_slider.dart';
|
||||
@ -53,7 +53,7 @@ class ConnectControlPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
|
||||
final ThemeData(:typography, :colorScheme) = Theme.of(context);
|
||||
|
||||
final resolvedService =
|
||||
ref.watch(connectClientsProvider).asData?.value.resolvedService;
|
||||
@ -63,23 +63,6 @@ class ConnectControlPage extends HookConsumerWidget {
|
||||
final shuffled = ref.watch(shuffleProvider);
|
||||
final loopMode = ref.watch(loopModeProvider);
|
||||
|
||||
final resumePauseStyle = IconButton.styleFrom(
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
padding: const EdgeInsets.all(12),
|
||||
iconSize: 24,
|
||||
);
|
||||
final buttonStyle = IconButton.styleFrom(
|
||||
backgroundColor: colorScheme.surface.withOpacity(0.4),
|
||||
minimumSize: const Size(28, 28),
|
||||
);
|
||||
|
||||
final activeButtonStyle = IconButton.styleFrom(
|
||||
backgroundColor: colorScheme.primaryContainer,
|
||||
foregroundColor: colorScheme.onPrimaryContainer,
|
||||
minimumSize: const Size(28, 28),
|
||||
);
|
||||
|
||||
ref.listen(connectClientsProvider, (prev, next) {
|
||||
if (next.asData?.value.resolvedService == null) {
|
||||
context.pop();
|
||||
@ -87,12 +70,15 @@ class ConnectControlPage extends HookConsumerWidget {
|
||||
});
|
||||
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: Scaffold(
|
||||
appBar: TitleBar(
|
||||
headers: [
|
||||
TitleBar(
|
||||
title: Text(resolvedService!.name),
|
||||
automaticallyImplyLeading: true,
|
||||
),
|
||||
body: LayoutBuilder(builder: (context, constrains) {
|
||||
)
|
||||
],
|
||||
child: LayoutBuilder(builder: (context, constrains) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@ -106,7 +92,7 @@ class ConnectControlPage extends HookConsumerWidget {
|
||||
vertical: 10,
|
||||
).copyWith(top: 0),
|
||||
constraints:
|
||||
const BoxConstraints(maxHeight: 400, maxWidth: 400),
|
||||
const BoxConstraints(maxHeight: 350, maxWidth: 350),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: UniversalImage(
|
||||
@ -126,7 +112,7 @@ class ConnectControlPage extends HookConsumerWidget {
|
||||
SliverToBoxAdapter(
|
||||
child: AnchorButton(
|
||||
playlist.activeTrack?.name ?? "",
|
||||
style: textTheme.titleLarge!,
|
||||
style: typography.h4,
|
||||
onTap: () {
|
||||
if (playlist.activeTrack == null) return;
|
||||
ServiceUtils.pushNamed(
|
||||
@ -142,7 +128,7 @@ class ConnectControlPage extends HookConsumerWidget {
|
||||
SliverToBoxAdapter(
|
||||
child: ArtistLink(
|
||||
artists: playlist.activeTrack?.artists ?? [],
|
||||
textStyle: textTheme.bodyMedium!,
|
||||
textStyle: typography.normal,
|
||||
mainAxisAlignment: WrapAlignment.start,
|
||||
onOverflowArtistClick: () =>
|
||||
ServiceUtils.pushNamed(
|
||||
@ -164,19 +150,25 @@ class ConnectControlPage extends HookConsumerWidget {
|
||||
final position = ref.watch(positionProvider);
|
||||
final duration = ref.watch(durationProvider);
|
||||
|
||||
final progress = duration.inSeconds == 0
|
||||
? 0
|
||||
: position.inSeconds / duration.inSeconds;
|
||||
|
||||
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(),
|
||||
value:
|
||||
SliderValue.single(progress.toDouble()),
|
||||
onChanged: (value) {
|
||||
connectNotifier
|
||||
.seek(Duration(seconds: value.toInt()));
|
||||
connectNotifier.seek(
|
||||
Duration(
|
||||
seconds:
|
||||
(value.value * duration.inSeconds)
|
||||
.toInt(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
@ -197,43 +189,59 @@ class ConnectControlPage extends HookConsumerWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: shuffled
|
||||
Tooltip(
|
||||
tooltip: TooltipContainer(
|
||||
child: Text(
|
||||
shuffled
|
||||
? context.l10n.unshuffle_playlist
|
||||
: context.l10n.shuffle_playlist,
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: const Icon(SpotubeIcons.shuffle),
|
||||
style: shuffled ? activeButtonStyle : buttonStyle,
|
||||
variance: shuffled
|
||||
? ButtonVariance.secondary
|
||||
: ButtonVariance.ghost,
|
||||
onPressed: playlist.activeTrack == null
|
||||
? null
|
||||
: () {
|
||||
connectNotifier.setShuffle(!shuffled);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
tooltip: context.l10n.previous_track,
|
||||
),
|
||||
Tooltip(
|
||||
tooltip: TooltipContainer(
|
||||
child: Text(context.l10n.previous_track),
|
||||
),
|
||||
child: IconButton.ghost(
|
||||
icon: const Icon(SpotubeIcons.skipBack),
|
||||
onPressed: playlist.activeTrack == null
|
||||
? null
|
||||
: connectNotifier.previous,
|
||||
),
|
||||
IconButton(
|
||||
tooltip: playing
|
||||
),
|
||||
Tooltip(
|
||||
tooltip: TooltipContainer(
|
||||
child: Text(
|
||||
playing
|
||||
? context.l10n.pause_playback
|
||||
: context.l10n.resume_playback,
|
||||
),
|
||||
),
|
||||
child: IconButton.primary(
|
||||
shape: ButtonShape.circle,
|
||||
icon: playlist.activeTrack == null
|
||||
? SizedBox(
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: colorScheme.onPrimary,
|
||||
),
|
||||
onSurface: false),
|
||||
)
|
||||
: Icon(
|
||||
playing
|
||||
? SpotubeIcons.pause
|
||||
: SpotubeIcons.play,
|
||||
),
|
||||
style: resumePauseStyle,
|
||||
onPressed: playlist.activeTrack == null
|
||||
? null
|
||||
: () {
|
||||
@ -244,28 +252,37 @@ class ConnectControlPage extends HookConsumerWidget {
|
||||
}
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
tooltip: context.l10n.next_track,
|
||||
),
|
||||
Tooltip(
|
||||
tooltip: TooltipContainer(
|
||||
child: Text(context.l10n.next_track)),
|
||||
child: IconButton.ghost(
|
||||
icon: const Icon(SpotubeIcons.skipForward),
|
||||
onPressed: playlist.activeTrack == null
|
||||
? null
|
||||
: connectNotifier.next,
|
||||
),
|
||||
IconButton(
|
||||
tooltip: loopMode == PlaylistMode.single
|
||||
),
|
||||
Tooltip(
|
||||
tooltip: TooltipContainer(
|
||||
child: Text(
|
||||
loopMode == PlaylistMode.single
|
||||
? context.l10n.loop_track
|
||||
: loopMode == PlaylistMode.loop
|
||||
? context.l10n.repeat_playlist
|
||||
: null,
|
||||
: context.l10n.no_loop,
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
loopMode == PlaylistMode.single
|
||||
? SpotubeIcons.repeatOne
|
||||
: SpotubeIcons.repeat,
|
||||
),
|
||||
style: loopMode == PlaylistMode.single ||
|
||||
variance: loopMode == PlaylistMode.single ||
|
||||
loopMode == PlaylistMode.loop
|
||||
? activeButtonStyle
|
||||
: buttonStyle,
|
||||
? ButtonVariance.secondary
|
||||
: ButtonVariance.ghost,
|
||||
onPressed: playlist.activeTrack == null
|
||||
? null
|
||||
: () async {
|
||||
@ -275,15 +292,52 @@ class ConnectControlPage extends HookConsumerWidget {
|
||||
PlaylistMode.single,
|
||||
PlaylistMode.single =>
|
||||
PlaylistMode.none,
|
||||
PlaylistMode.none => PlaylistMode.loop,
|
||||
PlaylistMode.none =>
|
||||
PlaylistMode.loop,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SliverGap(30),
|
||||
if (constrains.mdAndDown)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Button.outline(
|
||||
leading: const Icon(SpotubeIcons.queue),
|
||||
child: Text(context.l10n.queue),
|
||||
onPressed: () {
|
||||
openDrawer(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
draggable: true,
|
||||
barrierColor: Colors.black.withAlpha(100),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
transformBackdrop: false,
|
||||
position: OverlayPosition.bottom,
|
||||
surfaceBlur: context.theme.surfaceBlur,
|
||||
surfaceOpacity: 0.7,
|
||||
expands: true,
|
||||
builder: (context) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight:
|
||||
MediaQuery.sizeOf(context).height *
|
||||
0.8,
|
||||
),
|
||||
child: const RemotePlayerQueue(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverGap(30),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
sliver: SliverToBoxAdapter(
|
||||
@ -300,25 +354,7 @@ class ConnectControlPage extends HookConsumerWidget {
|
||||
}),
|
||||
),
|
||||
),
|
||||
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 const RemotePlayerQueue();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
const SliverSafeArea(sliver: SliverGap(10)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"ar": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -9,6 +10,7 @@
|
||||
],
|
||||
|
||||
"bn": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -18,6 +20,7 @@
|
||||
],
|
||||
|
||||
"ca": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -27,6 +30,7 @@
|
||||
],
|
||||
|
||||
"cs": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -36,6 +40,7 @@
|
||||
],
|
||||
|
||||
"de": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -45,6 +50,7 @@
|
||||
],
|
||||
|
||||
"es": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -54,6 +60,7 @@
|
||||
],
|
||||
|
||||
"eu": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -63,6 +70,7 @@
|
||||
],
|
||||
|
||||
"fa": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -72,6 +80,7 @@
|
||||
],
|
||||
|
||||
"fi": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -81,6 +90,7 @@
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -90,6 +100,7 @@
|
||||
],
|
||||
|
||||
"hi": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -99,6 +110,7 @@
|
||||
],
|
||||
|
||||
"id": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -108,6 +120,7 @@
|
||||
],
|
||||
|
||||
"it": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -117,6 +130,7 @@
|
||||
],
|
||||
|
||||
"ja": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -126,6 +140,7 @@
|
||||
],
|
||||
|
||||
"ka": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -135,6 +150,7 @@
|
||||
],
|
||||
|
||||
"ko": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -144,6 +160,7 @@
|
||||
],
|
||||
|
||||
"ne": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -153,6 +170,7 @@
|
||||
],
|
||||
|
||||
"nl": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -162,6 +180,7 @@
|
||||
],
|
||||
|
||||
"pl": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -171,6 +190,7 @@
|
||||
],
|
||||
|
||||
"pt": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -180,6 +200,7 @@
|
||||
],
|
||||
|
||||
"ru": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -189,6 +210,7 @@
|
||||
],
|
||||
|
||||
"th": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -198,6 +220,7 @@
|
||||
],
|
||||
|
||||
"tr": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -207,6 +230,7 @@
|
||||
],
|
||||
|
||||
"uk": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -216,6 +240,7 @@
|
||||
],
|
||||
|
||||
"vi": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
@ -225,6 +250,7 @@
|
||||
],
|
||||
|
||||
"zh": [
|
||||
"no_loop",
|
||||
"undo",
|
||||
"download_all",
|
||||
"add_all_to_playlist",
|
||||
|
Loading…
Reference in New Issue
Block a user