mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
fix(android): back button and safe area issues
This commit is contained in:
parent
6ddf6b9cce
commit
d4504722d8
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
@ -73,6 +74,10 @@ class TitleBar extends HookConsumerWidget implements PreferredSizeWidget {
|
|||||||
final hasFullscreen =
|
final hasFullscreen =
|
||||||
MediaQuery.sizeOf(context).width == constraints.maxWidth;
|
MediaQuery.sizeOf(context).width == constraints.maxWidth;
|
||||||
|
|
||||||
|
final canPop = leading.isEmpty &&
|
||||||
|
automaticallyImplyLeading &&
|
||||||
|
(Navigator.canPop(context) || context.watchRouter.canPop());
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onHorizontalDragStart: (_) => onDrag(ref),
|
onHorizontalDragStart: (_) => onDrag(ref),
|
||||||
onVerticalDragStart: (_) => onDrag(ref),
|
onVerticalDragStart: (_) => onDrag(ref),
|
||||||
@ -94,13 +99,7 @@ class TitleBar extends HookConsumerWidget implements PreferredSizeWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: AppBar(
|
child: AppBar(
|
||||||
leading: leading.isEmpty &&
|
leading: canPop ? [const BackButton()] : leading,
|
||||||
automaticallyImplyLeading &&
|
|
||||||
Navigator.canPop(context)
|
|
||||||
? [
|
|
||||||
const BackButton(),
|
|
||||||
]
|
|
||||||
: leading,
|
|
||||||
trailing: [
|
trailing: [
|
||||||
...trailing,
|
...trailing,
|
||||||
Align(
|
Align(
|
||||||
|
@ -23,65 +23,65 @@ class ConnectPage extends HookConsumerWidget {
|
|||||||
final connectClientsNotifier = ref.read(connectClientsProvider.notifier);
|
final connectClientsNotifier = ref.read(connectClientsProvider.notifier);
|
||||||
final discoveredDevices = connectClients.asData?.value.services;
|
final discoveredDevices = connectClients.asData?.value.services;
|
||||||
|
|
||||||
return Scaffold(
|
return SafeArea(
|
||||||
headers: [
|
bottom: false,
|
||||||
TitleBar(
|
child: Scaffold(
|
||||||
automaticallyImplyLeading: true,
|
headers: [
|
||||||
title: Text(context.l10n.devices),
|
TitleBar(title: Text(context.l10n.devices)),
|
||||||
)
|
],
|
||||||
],
|
child: Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.all(10.0),
|
||||||
padding: const EdgeInsets.all(10.0),
|
child: CustomScrollView(
|
||||||
child: CustomScrollView(
|
slivers: [
|
||||||
slivers: [
|
SliverPadding(
|
||||||
SliverPadding(
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
sliver: SliverToBoxAdapter(
|
||||||
sliver: SliverToBoxAdapter(
|
child: Text(
|
||||||
child: Text(
|
context.l10n.remote,
|
||||||
context.l10n.remote,
|
style: typography.bold,
|
||||||
style: typography.bold,
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SliverGap(10),
|
||||||
const SliverGap(10),
|
SliverList.separated(
|
||||||
SliverList.separated(
|
itemCount: discoveredDevices?.length ?? 0,
|
||||||
itemCount: discoveredDevices?.length ?? 0,
|
separatorBuilder: (context, index) => const Gap(10),
|
||||||
separatorBuilder: (context, index) => const Gap(10),
|
itemBuilder: (context, index) {
|
||||||
itemBuilder: (context, index) {
|
final device = discoveredDevices![index];
|
||||||
final device = discoveredDevices![index];
|
final selected =
|
||||||
final selected =
|
connectClients.asData?.value.resolvedService?.name ==
|
||||||
connectClients.asData?.value.resolvedService?.name ==
|
device.name;
|
||||||
device.name;
|
return ButtonTile(
|
||||||
return ButtonTile(
|
selected: selected,
|
||||||
selected: selected,
|
leading: const Icon(SpotubeIcons.monitor),
|
||||||
leading: const Icon(SpotubeIcons.monitor),
|
title: Text(device.name),
|
||||||
title: Text(device.name),
|
subtitle: selected
|
||||||
subtitle: selected
|
? Text(
|
||||||
? Text(
|
"${connectClients.asData?.value.resolvedService?.host}"
|
||||||
"${connectClients.asData?.value.resolvedService?.host}"
|
":${connectClients.asData?.value.resolvedService?.port}",
|
||||||
":${connectClients.asData?.value.resolvedService?.port}",
|
)
|
||||||
)
|
: null,
|
||||||
: null,
|
trailing: selected
|
||||||
trailing: selected
|
? IconButton.outline(
|
||||||
? IconButton.outline(
|
icon: const Icon(SpotubeIcons.power),
|
||||||
icon: const Icon(SpotubeIcons.power),
|
size: ButtonSize.small,
|
||||||
size: ButtonSize.small,
|
onPressed: () =>
|
||||||
onPressed: () =>
|
connectClientsNotifier.clearResolvedService(),
|
||||||
connectClientsNotifier.clearResolvedService(),
|
)
|
||||||
)
|
: null,
|
||||||
: null,
|
onPressed: () {
|
||||||
onPressed: () {
|
if (selected) {
|
||||||
if (selected) {
|
context.navigateTo(const ConnectControlRoute());
|
||||||
context.navigateTo(const ConnectControlRoute());
|
} else {
|
||||||
} else {
|
connectClientsNotifier.resolveService(device);
|
||||||
connectClientsNotifier.resolveService(device);
|
}
|
||||||
}
|
},
|
||||||
},
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
const ConnectPageLocalDevices(),
|
||||||
const ConnectPageLocalDevices(),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -75,7 +75,6 @@ class ConnectControlPage extends HookConsumerWidget {
|
|||||||
headers: [
|
headers: [
|
||||||
TitleBar(
|
TitleBar(
|
||||||
title: Text(resolvedService!.name),
|
title: Text(resolvedService!.name),
|
||||||
automaticallyImplyLeading: true,
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
child: LayoutBuilder(builder: (context, constrains) {
|
child: LayoutBuilder(builder: (context, constrains) {
|
||||||
|
@ -28,68 +28,71 @@ class HomeFeedSectionPage extends HookConsumerWidget {
|
|||||||
final controller = useScrollController();
|
final controller = useScrollController();
|
||||||
final isArtist = section.items.every((item) => item.artist != null);
|
final isArtist = section.items.every((item) => item.artist != null);
|
||||||
|
|
||||||
return Skeletonizer(
|
return SafeArea(
|
||||||
enabled: homeFeedSection.isLoading,
|
bottom: false,
|
||||||
child: Scaffold(
|
child: Skeletonizer(
|
||||||
headers: [
|
enabled: homeFeedSection.isLoading,
|
||||||
TitleBar(
|
child: Scaffold(
|
||||||
title: Text(section.title ?? ""),
|
headers: [
|
||||||
automaticallyImplyLeading: true,
|
TitleBar(
|
||||||
)
|
title: Text(section.title ?? ""),
|
||||||
],
|
)
|
||||||
child: Padding(
|
],
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
child: Padding(
|
||||||
child: CustomScrollView(
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
controller: controller,
|
child: CustomScrollView(
|
||||||
slivers: [
|
controller: controller,
|
||||||
if (isArtist)
|
slivers: [
|
||||||
SliverGrid.builder(
|
if (isArtist)
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
SliverGrid.builder(
|
||||||
maxCrossAxisExtent: 200,
|
gridDelegate:
|
||||||
mainAxisExtent: 250,
|
const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
crossAxisSpacing: 8,
|
maxCrossAxisExtent: 200,
|
||||||
mainAxisSpacing: 8,
|
mainAxisExtent: 250,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
),
|
||||||
|
itemCount: section.items.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = section.items[index];
|
||||||
|
return ArtistCard(item.artist!.asArtist);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else
|
||||||
|
PlaybuttonView(
|
||||||
|
controller: controller,
|
||||||
|
itemCount: section.items.length,
|
||||||
|
hasMore: false,
|
||||||
|
isLoading: false,
|
||||||
|
onRequestMore: () => {},
|
||||||
|
listItemBuilder: (context, index) {
|
||||||
|
final item = section.items[index];
|
||||||
|
if (item.album != null) {
|
||||||
|
return AlbumCard.tile(item.album!.asAlbum);
|
||||||
|
}
|
||||||
|
if (item.playlist != null) {
|
||||||
|
return PlaylistCard.tile(item.playlist!.asPlaylist);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
gridItemBuilder: (context, index) {
|
||||||
|
final item = section.items[index];
|
||||||
|
if (item.album != null) {
|
||||||
|
return AlbumCard(item.album!.asAlbum);
|
||||||
|
}
|
||||||
|
if (item.playlist != null) {
|
||||||
|
return PlaylistCard(item.playlist!.asPlaylist);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: SafeArea(
|
||||||
|
child: SizedBox(),
|
||||||
),
|
),
|
||||||
itemCount: section.items.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final item = section.items[index];
|
|
||||||
return ArtistCard(item.artist!.asArtist);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
else
|
|
||||||
PlaybuttonView(
|
|
||||||
controller: controller,
|
|
||||||
itemCount: section.items.length,
|
|
||||||
hasMore: false,
|
|
||||||
isLoading: false,
|
|
||||||
onRequestMore: () => {},
|
|
||||||
listItemBuilder: (context, index) {
|
|
||||||
final item = section.items[index];
|
|
||||||
if (item.album != null) {
|
|
||||||
return AlbumCard.tile(item.album!.asAlbum);
|
|
||||||
}
|
|
||||||
if (item.playlist != null) {
|
|
||||||
return PlaylistCard.tile(item.playlist!.asPlaylist);
|
|
||||||
}
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
},
|
|
||||||
gridItemBuilder: (context, index) {
|
|
||||||
final item = section.items[index];
|
|
||||||
if (item.album != null) {
|
|
||||||
return AlbumCard(item.album!.asAlbum);
|
|
||||||
}
|
|
||||||
if (item.playlist != null) {
|
|
||||||
return PlaylistCard(item.playlist!.asPlaylist);
|
|
||||||
}
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SliverToBoxAdapter(
|
],
|
||||||
child: SafeArea(
|
),
|
||||||
child: SizedBox(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -45,93 +45,98 @@ class GenrePlaylistsPage extends HookConsumerWidget {
|
|||||||
automaticSystemUiAdjustment: false,
|
automaticSystemUiAdjustment: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return SafeArea(
|
||||||
headers: [
|
child: Scaffold(
|
||||||
if (kIsDesktop)
|
headers: [
|
||||||
const TitleBar(
|
if (kIsDesktop)
|
||||||
leading: [
|
const TitleBar(
|
||||||
BackButton(),
|
leading: [
|
||||||
],
|
BackButton(),
|
||||||
backgroundColor: Colors.transparent,
|
],
|
||||||
surfaceOpacity: 0,
|
backgroundColor: Colors.transparent,
|
||||||
surfaceBlur: 0,
|
surfaceOpacity: 0,
|
||||||
)
|
surfaceBlur: 0,
|
||||||
],
|
)
|
||||||
floatingHeader: true,
|
],
|
||||||
child: DecoratedBox(
|
floatingHeader: true,
|
||||||
decoration: BoxDecoration(
|
child: DecoratedBox(
|
||||||
image: DecorationImage(
|
decoration: BoxDecoration(
|
||||||
image: UniversalImage.imageProvider(category.icons!.first.url!),
|
image: DecorationImage(
|
||||||
alignment: Alignment.topCenter,
|
image: UniversalImage.imageProvider(category.icons!.first.url!),
|
||||||
fit: BoxFit.cover,
|
alignment: Alignment.topCenter,
|
||||||
repeat: ImageRepeat.noRepeat,
|
fit: BoxFit.cover,
|
||||||
matchTextDirection: true,
|
repeat: ImageRepeat.noRepeat,
|
||||||
|
matchTextDirection: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
child: SurfaceCard(
|
||||||
child: SurfaceCard(
|
borderRadius: BorderRadius.zero,
|
||||||
borderRadius: BorderRadius.zero,
|
padding: EdgeInsets.zero,
|
||||||
padding: EdgeInsets.zero,
|
child: CustomScrollView(
|
||||||
child: CustomScrollView(
|
controller: scrollController,
|
||||||
controller: scrollController,
|
slivers: [
|
||||||
slivers: [
|
SliverSafeArea(
|
||||||
SliverAppBar(
|
bottom: false,
|
||||||
automaticallyImplyLeading: false,
|
sliver: SliverAppBar(
|
||||||
leading: kIsMobile ? const BackButton() : null,
|
automaticallyImplyLeading: false,
|
||||||
expandedHeight: mediaQuery.mdAndDown ? 200 : 150,
|
leading: kIsMobile ? const BackButton() : null,
|
||||||
title: const Text(""),
|
expandedHeight: mediaQuery.mdAndDown ? 200 : 150,
|
||||||
backgroundColor: Colors.transparent,
|
title: const Text(""),
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
backgroundColor: Colors.transparent,
|
||||||
centerTitle: kIsDesktop,
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
title: Text(
|
centerTitle: kIsDesktop,
|
||||||
category.name!,
|
title: Text(
|
||||||
style: context.theme.typography.h3.copyWith(
|
category.name!,
|
||||||
color: Colors.white,
|
style: context.theme.typography.h3.copyWith(
|
||||||
letterSpacing: 3,
|
color: Colors.white,
|
||||||
shadows: [
|
letterSpacing: 3,
|
||||||
Shadow(
|
shadows: [
|
||||||
offset: const Offset(-1.5, -1.5),
|
Shadow(
|
||||||
color: Colors.black.withAlpha(138),
|
offset: const Offset(-1.5, -1.5),
|
||||||
|
color: Colors.black.withAlpha(138),
|
||||||
|
),
|
||||||
|
Shadow(
|
||||||
|
offset: const Offset(1.5, -1.5),
|
||||||
|
color: Colors.black.withAlpha(138),
|
||||||
|
),
|
||||||
|
Shadow(
|
||||||
|
offset: const Offset(1.5, 1.5),
|
||||||
|
color: Colors.black.withAlpha(138),
|
||||||
|
),
|
||||||
|
Shadow(
|
||||||
|
offset: const Offset(-1.5, 1.5),
|
||||||
|
color: Colors.black.withAlpha(138),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
Shadow(
|
),
|
||||||
offset: const Offset(1.5, -1.5),
|
collapseMode: CollapseMode.parallax,
|
||||||
color: Colors.black.withAlpha(138),
|
|
||||||
),
|
|
||||||
Shadow(
|
|
||||||
offset: const Offset(1.5, 1.5),
|
|
||||||
color: Colors.black.withAlpha(138),
|
|
||||||
),
|
|
||||||
Shadow(
|
|
||||||
offset: const Offset(-1.5, 1.5),
|
|
||||||
color: Colors.black.withAlpha(138),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
collapseMode: CollapseMode.parallax,
|
|
||||||
),
|
),
|
||||||
),
|
const SliverGap(20),
|
||||||
const SliverGap(20),
|
SliverSafeArea(
|
||||||
SliverSafeArea(
|
top: false,
|
||||||
top: false,
|
sliver: SliverPadding(
|
||||||
sliver: SliverPadding(
|
padding: EdgeInsets.symmetric(
|
||||||
padding: EdgeInsets.symmetric(
|
horizontal: mediaQuery.mdAndDown ? 12 : 24,
|
||||||
horizontal: mediaQuery.mdAndDown ? 12 : 24,
|
),
|
||||||
),
|
sliver: PlaybuttonView(
|
||||||
sliver: PlaybuttonView(
|
controller: scrollController,
|
||||||
controller: scrollController,
|
itemCount: playlists.asData?.value.items.length ?? 0,
|
||||||
itemCount: playlists.asData?.value.items.length ?? 0,
|
isLoading: playlists.isLoading,
|
||||||
isLoading: playlists.isLoading,
|
hasMore: playlists.asData?.value.hasMore == true,
|
||||||
hasMore: playlists.asData?.value.hasMore == true,
|
onRequestMore: playlistsNotifier.fetchMore,
|
||||||
onRequestMore: playlistsNotifier.fetchMore,
|
listItemBuilder: (context, index) => PlaylistCard.tile(
|
||||||
listItemBuilder: (context, index) =>
|
playlists.asData!.value.items[index]),
|
||||||
PlaylistCard.tile(playlists.asData!.value.items[index]),
|
gridItemBuilder: (context, index) =>
|
||||||
gridItemBuilder: (context, index) =>
|
PlaylistCard(playlists.asData!.value.items[index]),
|
||||||
PlaylistCard(playlists.asData!.value.items[index]),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SliverGap(20),
|
||||||
const SliverGap(20),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -32,7 +32,6 @@ class GenrePage extends HookConsumerWidget {
|
|||||||
headers: [
|
headers: [
|
||||||
TitleBar(
|
TitleBar(
|
||||||
title: Text(context.l10n.explore_genres),
|
title: Text(context.l10n.explore_genres),
|
||||||
automaticallyImplyLeading: true,
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
child: GridView.builder(
|
child: GridView.builder(
|
||||||
|
@ -31,6 +31,7 @@ class LastFMLoginPage extends HookConsumerWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
headers: const [
|
headers: const [
|
||||||
SafeArea(
|
SafeArea(
|
||||||
|
bottom: false,
|
||||||
child: TitleBar(
|
child: TitleBar(
|
||||||
leading: [BackButton()],
|
leading: [BackButton()],
|
||||||
),
|
),
|
||||||
@ -39,102 +40,104 @@ class LastFMLoginPage extends HookConsumerWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Flexible(
|
||||||
constraints: const BoxConstraints(maxWidth: 400),
|
child: Container(
|
||||||
alignment: Alignment.center,
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
padding: const EdgeInsets.all(16),
|
alignment: Alignment.center,
|
||||||
child: Card(
|
padding: const EdgeInsets.all(16),
|
||||||
padding: const EdgeInsets.all(16.0),
|
child: Card(
|
||||||
child: Form(
|
padding: const EdgeInsets.all(16.0),
|
||||||
onSubmit: (context, values) async {
|
child: Form(
|
||||||
try {
|
onSubmit: (context, values) async {
|
||||||
isLoading.value = true;
|
try {
|
||||||
await scrobblerNotifier.login(
|
isLoading.value = true;
|
||||||
values[usernameKey].trim(),
|
await scrobblerNotifier.login(
|
||||||
values[passwordKey],
|
values[usernameKey].trim(),
|
||||||
);
|
values[passwordKey],
|
||||||
if (context.mounted) {
|
|
||||||
context.back();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (context.mounted) {
|
|
||||||
showPromptDialog(
|
|
||||||
context: context,
|
|
||||||
title: context.l10n.error("Authentication failed"),
|
|
||||||
message: e.toString(),
|
|
||||||
cancelText: null,
|
|
||||||
);
|
);
|
||||||
|
if (context.mounted) {
|
||||||
|
context.back();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
showPromptDialog(
|
||||||
|
context: context,
|
||||||
|
title: context.l10n.error("Authentication failed"),
|
||||||
|
message: e.toString(),
|
||||||
|
cancelText: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
} finally {
|
},
|
||||||
isLoading.value = false;
|
child: Column(
|
||||||
}
|
mainAxisSize: MainAxisSize.min,
|
||||||
},
|
spacing: 10,
|
||||||
child: Column(
|
children: [
|
||||||
mainAxisSize: MainAxisSize.min,
|
Container(
|
||||||
spacing: 10,
|
decoration: BoxDecoration(
|
||||||
children: [
|
borderRadius: BorderRadius.circular(30),
|
||||||
Container(
|
color: const Color.fromARGB(255, 186, 0, 0),
|
||||||
decoration: BoxDecoration(
|
),
|
||||||
borderRadius: BorderRadius.circular(30),
|
padding: const EdgeInsets.all(12),
|
||||||
color: const Color.fromARGB(255, 186, 0, 0),
|
child: const Icon(
|
||||||
|
SpotubeIcons.lastFm,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 60,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.all(12),
|
const Text("last.fm").h3(),
|
||||||
child: const Icon(
|
Text(context.l10n.login_with_your_lastfm),
|
||||||
SpotubeIcons.lastFm,
|
AutofillGroup(
|
||||||
color: Colors.white,
|
child: Column(
|
||||||
size: 60,
|
spacing: 10,
|
||||||
),
|
children: [
|
||||||
),
|
FormField(
|
||||||
const Text("last.fm").h3(),
|
label: Text(context.l10n.username),
|
||||||
Text(context.l10n.login_with_your_lastfm),
|
key: usernameKey,
|
||||||
AutofillGroup(
|
validator: const NotEmptyValidator(),
|
||||||
child: Column(
|
child: TextField(
|
||||||
spacing: 10,
|
autofillHints: const [
|
||||||
children: [
|
AutofillHints.username,
|
||||||
FormField(
|
AutofillHints.email,
|
||||||
label: Text(context.l10n.username),
|
],
|
||||||
key: usernameKey,
|
placeholder: Text(context.l10n.username),
|
||||||
validator: const NotEmptyValidator(),
|
|
||||||
child: TextField(
|
|
||||||
autofillHints: const [
|
|
||||||
AutofillHints.username,
|
|
||||||
AutofillHints.email,
|
|
||||||
],
|
|
||||||
placeholder: Text(context.l10n.username),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FormField(
|
|
||||||
key: passwordKey,
|
|
||||||
validator: const NotEmptyValidator(),
|
|
||||||
label: Text(context.l10n.password),
|
|
||||||
child: TextField(
|
|
||||||
autofillHints: const [
|
|
||||||
AutofillHints.password,
|
|
||||||
],
|
|
||||||
obscureText: !passwordVisible.value,
|
|
||||||
placeholder: Text(context.l10n.password),
|
|
||||||
trailing: IconButton.ghost(
|
|
||||||
icon: Icon(
|
|
||||||
passwordVisible.value
|
|
||||||
? SpotubeIcons.eye
|
|
||||||
: SpotubeIcons.noEye,
|
|
||||||
),
|
|
||||||
onPressed: () => passwordVisible.value =
|
|
||||||
!passwordVisible.value,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
FormField(
|
||||||
],
|
key: passwordKey,
|
||||||
|
validator: const NotEmptyValidator(),
|
||||||
|
label: Text(context.l10n.password),
|
||||||
|
child: TextField(
|
||||||
|
autofillHints: const [
|
||||||
|
AutofillHints.password,
|
||||||
|
],
|
||||||
|
obscureText: !passwordVisible.value,
|
||||||
|
placeholder: Text(context.l10n.password),
|
||||||
|
trailing: IconButton.ghost(
|
||||||
|
icon: Icon(
|
||||||
|
passwordVisible.value
|
||||||
|
? SpotubeIcons.eye
|
||||||
|
: SpotubeIcons.noEye,
|
||||||
|
),
|
||||||
|
onPressed: () => passwordVisible.value =
|
||||||
|
!passwordVisible.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
FormErrorBuilder(builder: (context, errors, child) {
|
||||||
FormErrorBuilder(builder: (context, errors, child) {
|
return Button.primary(
|
||||||
return Button.primary(
|
onPressed: () => context.submitForm(),
|
||||||
onPressed: () => context.submitForm(),
|
enabled: errors.isEmpty && !isLoading.value,
|
||||||
enabled: errors.isEmpty && !isLoading.value,
|
child: Text(context.l10n.login),
|
||||||
child: Text(context.l10n.login),
|
);
|
||||||
);
|
}),
|
||||||
}),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -256,426 +256,430 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final controller = useScrollController();
|
final controller = useScrollController();
|
||||||
|
|
||||||
return Scaffold(
|
return SafeArea(
|
||||||
headers: [
|
bottom: false,
|
||||||
TitleBar(
|
child: Scaffold(
|
||||||
leading: const [BackButton()],
|
headers: [
|
||||||
title: Text(context.l10n.generate),
|
TitleBar(
|
||||||
)
|
leading: const [BackButton()],
|
||||||
],
|
title: Text(context.l10n.generate),
|
||||||
child: Scrollbar(
|
)
|
||||||
controller: controller,
|
],
|
||||||
child: Center(
|
child: Scrollbar(
|
||||||
child: ConstrainedBox(
|
controller: controller,
|
||||||
constraints: BoxConstraints(maxWidth: Breakpoints.lg),
|
child: Center(
|
||||||
child: SafeArea(
|
child: ConstrainedBox(
|
||||||
child: LayoutBuilder(builder: (context, constrains) {
|
constraints: BoxConstraints(maxWidth: Breakpoints.lg),
|
||||||
return ScrollConfiguration(
|
child: SafeArea(
|
||||||
behavior: ScrollConfiguration.of(context)
|
child: LayoutBuilder(builder: (context, constrains) {
|
||||||
.copyWith(scrollbars: false),
|
return ScrollConfiguration(
|
||||||
child: ListView(
|
behavior: ScrollConfiguration.of(context)
|
||||||
controller: controller,
|
.copyWith(scrollbars: false),
|
||||||
padding: const EdgeInsets.all(16),
|
child: ListView(
|
||||||
children: [
|
controller: controller,
|
||||||
ValueListenableBuilder(
|
padding: const EdgeInsets.all(16),
|
||||||
valueListenable: limit,
|
children: [
|
||||||
builder: (context, value, child) {
|
ValueListenableBuilder(
|
||||||
return Column(
|
valueListenable: limit,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
builder: (context, value, child) {
|
||||||
children: [
|
return Column(
|
||||||
Text(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
context.l10n.number_of_tracks_generate,
|
children: [
|
||||||
style: typography.semiBold,
|
Text(
|
||||||
),
|
context.l10n.number_of_tracks_generate,
|
||||||
Row(
|
style: typography.semiBold,
|
||||||
spacing: 5,
|
),
|
||||||
children: [
|
Row(
|
||||||
Container(
|
spacing: 5,
|
||||||
width: 40,
|
children: [
|
||||||
height: 40,
|
Container(
|
||||||
alignment: Alignment.center,
|
width: 40,
|
||||||
decoration: BoxDecoration(
|
height: 40,
|
||||||
color: theme.colorScheme.primary
|
alignment: Alignment.center,
|
||||||
.withAlpha(25),
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
color: theme.colorScheme.primary
|
||||||
),
|
.withAlpha(25),
|
||||||
child: Text(
|
shape: BoxShape.circle,
|
||||||
value.round().toString(),
|
),
|
||||||
style: typography.large.copyWith(
|
child: Text(
|
||||||
color: theme.colorScheme.primary,
|
value.round().toString(),
|
||||||
|
style: typography.large.copyWith(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
Expanded(
|
child: Slider(
|
||||||
child: Slider(
|
value: SliderValue.single(
|
||||||
value:
|
value.toDouble()),
|
||||||
SliderValue.single(value.toDouble()),
|
min: 10,
|
||||||
min: 10,
|
max: 100,
|
||||||
max: 100,
|
divisions: 9,
|
||||||
divisions: 9,
|
onChanged: (value) {
|
||||||
onChanged: (value) {
|
limit.value = value.value.round();
|
||||||
limit.value = value.value.round();
|
},
|
||||||
},
|
),
|
||||||
),
|
)
|
||||||
)
|
],
|
||||||
],
|
)
|
||||||
)
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (constrains.mdAndUp)
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: countrySelector,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: genreSelector,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
)
|
||||||
},
|
else ...[
|
||||||
),
|
countrySelector,
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (constrains.mdAndUp)
|
genreSelector,
|
||||||
Row(
|
],
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: countrySelector,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: genreSelector,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
else ...[
|
|
||||||
countrySelector,
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
genreSelector,
|
if (constrains.mdAndUp)
|
||||||
],
|
Row(
|
||||||
const SizedBox(height: 16),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
if (constrains.mdAndUp)
|
children: [
|
||||||
Row(
|
Expanded(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: artistAutoComplete,
|
||||||
children: [
|
),
|
||||||
Expanded(
|
const SizedBox(width: 16),
|
||||||
child: artistAutoComplete,
|
Expanded(
|
||||||
),
|
child: tracksAutocomplete,
|
||||||
const SizedBox(width: 16),
|
),
|
||||||
Expanded(
|
],
|
||||||
child: tracksAutocomplete,
|
)
|
||||||
),
|
else ...[
|
||||||
],
|
artistAutoComplete,
|
||||||
)
|
const SizedBox(height: 16),
|
||||||
else ...[
|
tracksAutocomplete,
|
||||||
artistAutoComplete,
|
],
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
tracksAutocomplete,
|
RecommendationAttributeDials(
|
||||||
],
|
title: Text(context.l10n.acousticness),
|
||||||
const SizedBox(height: 16),
|
values: (
|
||||||
RecommendationAttributeDials(
|
target: target.value.acousticness?.toDouble() ?? 0,
|
||||||
title: Text(context.l10n.acousticness),
|
min: min.value.acousticness?.toDouble() ?? 0,
|
||||||
values: (
|
max: max.value.acousticness?.toDouble() ?? 0,
|
||||||
target: target.value.acousticness?.toDouble() ?? 0,
|
|
||||||
min: min.value.acousticness?.toDouble() ?? 0,
|
|
||||||
max: max.value.acousticness?.toDouble() ?? 0,
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
target.value = target.value.copyWith(
|
|
||||||
acousticness: value.target,
|
|
||||||
);
|
|
||||||
min.value = min.value.copyWith(
|
|
||||||
acousticness: value.min,
|
|
||||||
);
|
|
||||||
max.value = max.value.copyWith(
|
|
||||||
acousticness: value.max,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
RecommendationAttributeDials(
|
|
||||||
title: Text(context.l10n.danceability),
|
|
||||||
values: (
|
|
||||||
target: target.value.danceability?.toDouble() ?? 0,
|
|
||||||
min: min.value.danceability?.toDouble() ?? 0,
|
|
||||||
max: max.value.danceability?.toDouble() ?? 0,
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
target.value = target.value.copyWith(
|
|
||||||
danceability: value.target,
|
|
||||||
);
|
|
||||||
min.value = min.value.copyWith(
|
|
||||||
danceability: value.min,
|
|
||||||
);
|
|
||||||
max.value = max.value.copyWith(
|
|
||||||
danceability: value.max,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
RecommendationAttributeDials(
|
|
||||||
title: Text(context.l10n.energy),
|
|
||||||
values: (
|
|
||||||
target: target.value.energy?.toDouble() ?? 0,
|
|
||||||
min: min.value.energy?.toDouble() ?? 0,
|
|
||||||
max: max.value.energy?.toDouble() ?? 0,
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
target.value = target.value.copyWith(
|
|
||||||
energy: value.target,
|
|
||||||
);
|
|
||||||
min.value = min.value.copyWith(
|
|
||||||
energy: value.min,
|
|
||||||
);
|
|
||||||
max.value = max.value.copyWith(
|
|
||||||
energy: value.max,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
RecommendationAttributeDials(
|
|
||||||
title: Text(context.l10n.instrumentalness),
|
|
||||||
values: (
|
|
||||||
target:
|
|
||||||
target.value.instrumentalness?.toDouble() ?? 0,
|
|
||||||
min: min.value.instrumentalness?.toDouble() ?? 0,
|
|
||||||
max: max.value.instrumentalness?.toDouble() ?? 0,
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
target.value = target.value.copyWith(
|
|
||||||
instrumentalness: value.target,
|
|
||||||
);
|
|
||||||
min.value = min.value.copyWith(
|
|
||||||
instrumentalness: value.min,
|
|
||||||
);
|
|
||||||
max.value = max.value.copyWith(
|
|
||||||
instrumentalness: value.max,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
RecommendationAttributeDials(
|
|
||||||
title: Text(context.l10n.liveness),
|
|
||||||
values: (
|
|
||||||
target: target.value.liveness?.toDouble() ?? 0,
|
|
||||||
min: min.value.liveness?.toDouble() ?? 0,
|
|
||||||
max: max.value.liveness?.toDouble() ?? 0,
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
target.value = target.value.copyWith(
|
|
||||||
liveness: value.target,
|
|
||||||
);
|
|
||||||
min.value = min.value.copyWith(
|
|
||||||
liveness: value.min,
|
|
||||||
);
|
|
||||||
max.value = max.value.copyWith(
|
|
||||||
liveness: value.max,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
RecommendationAttributeDials(
|
|
||||||
title: Text(context.l10n.loudness),
|
|
||||||
values: (
|
|
||||||
target: target.value.loudness?.toDouble() ?? 0,
|
|
||||||
min: min.value.loudness?.toDouble() ?? 0,
|
|
||||||
max: max.value.loudness?.toDouble() ?? 0,
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
target.value = target.value.copyWith(
|
|
||||||
loudness: value.target,
|
|
||||||
);
|
|
||||||
min.value = min.value.copyWith(
|
|
||||||
loudness: value.min,
|
|
||||||
);
|
|
||||||
max.value = max.value.copyWith(
|
|
||||||
loudness: value.max,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
RecommendationAttributeDials(
|
|
||||||
title: Text(context.l10n.speechiness),
|
|
||||||
values: (
|
|
||||||
target: target.value.speechiness?.toDouble() ?? 0,
|
|
||||||
min: min.value.speechiness?.toDouble() ?? 0,
|
|
||||||
max: max.value.speechiness?.toDouble() ?? 0,
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
target.value = target.value.copyWith(
|
|
||||||
speechiness: value.target,
|
|
||||||
);
|
|
||||||
min.value = min.value.copyWith(
|
|
||||||
speechiness: value.min,
|
|
||||||
);
|
|
||||||
max.value = max.value.copyWith(
|
|
||||||
speechiness: value.max,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
RecommendationAttributeDials(
|
|
||||||
title: Text(context.l10n.valence),
|
|
||||||
values: (
|
|
||||||
target: target.value.valence?.toDouble() ?? 0,
|
|
||||||
min: min.value.valence?.toDouble() ?? 0,
|
|
||||||
max: max.value.valence?.toDouble() ?? 0,
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
target.value = target.value.copyWith(
|
|
||||||
valence: value.target,
|
|
||||||
);
|
|
||||||
min.value = min.value.copyWith(
|
|
||||||
valence: value.min,
|
|
||||||
);
|
|
||||||
max.value = max.value.copyWith(
|
|
||||||
valence: value.max,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
RecommendationAttributeDials(
|
|
||||||
title: Text(context.l10n.popularity),
|
|
||||||
base: 100,
|
|
||||||
values: (
|
|
||||||
target: target.value.popularity?.toDouble() ?? 0,
|
|
||||||
min: min.value.popularity?.toDouble() ?? 0,
|
|
||||||
max: max.value.popularity?.toDouble() ?? 0,
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
target.value = target.value.copyWith(
|
|
||||||
popularity: value.target,
|
|
||||||
);
|
|
||||||
min.value = min.value.copyWith(
|
|
||||||
popularity: value.min,
|
|
||||||
);
|
|
||||||
max.value = max.value.copyWith(
|
|
||||||
popularity: value.max,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
RecommendationAttributeDials(
|
|
||||||
title: Text(context.l10n.key),
|
|
||||||
base: 11,
|
|
||||||
values: (
|
|
||||||
target: target.value.key?.toDouble() ?? 0,
|
|
||||||
min: min.value.key?.toDouble() ?? 0,
|
|
||||||
max: max.value.key?.toDouble() ?? 0,
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
target.value = target.value.copyWith(
|
|
||||||
key: value.target,
|
|
||||||
);
|
|
||||||
min.value = min.value.copyWith(
|
|
||||||
key: value.min,
|
|
||||||
);
|
|
||||||
max.value = max.value.copyWith(
|
|
||||||
key: value.max,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
RecommendationAttributeFields(
|
|
||||||
title: Text(context.l10n.duration),
|
|
||||||
values: (
|
|
||||||
max: (max.value.durationMs ?? 0) / 1000,
|
|
||||||
target: (target.value.durationMs ?? 0) / 1000,
|
|
||||||
min: (min.value.durationMs ?? 0) / 1000,
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
target.value = target.value.copyWith(
|
|
||||||
durationMs: (value.target * 1000).toInt(),
|
|
||||||
);
|
|
||||||
min.value = min.value.copyWith(
|
|
||||||
durationMs: (value.min * 1000).toInt(),
|
|
||||||
);
|
|
||||||
max.value = max.value.copyWith(
|
|
||||||
durationMs: (value.max * 1000).toInt(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
presets: {
|
|
||||||
context.l10n.short: (min: 50, target: 90, max: 120),
|
|
||||||
context.l10n.medium: (
|
|
||||||
min: 120,
|
|
||||||
target: 180,
|
|
||||||
max: 200
|
|
||||||
),
|
),
|
||||||
context.l10n.long: (min: 480, target: 560, max: 640)
|
onChanged: (value) {
|
||||||
},
|
target.value = target.value.copyWith(
|
||||||
),
|
acousticness: value.target,
|
||||||
RecommendationAttributeFields(
|
);
|
||||||
title: Text(context.l10n.tempo),
|
min.value = min.value.copyWith(
|
||||||
values: (
|
acousticness: value.min,
|
||||||
max: max.value.tempo?.toDouble() ?? 0,
|
);
|
||||||
target: target.value.tempo?.toDouble() ?? 0,
|
max.value = max.value.copyWith(
|
||||||
min: min.value.tempo?.toDouble() ?? 0,
|
acousticness: value.max,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
RecommendationAttributeDials(
|
||||||
target.value = target.value.copyWith(
|
title: Text(context.l10n.danceability),
|
||||||
tempo: value.target,
|
values: (
|
||||||
);
|
target: target.value.danceability?.toDouble() ?? 0,
|
||||||
min.value = min.value.copyWith(
|
min: min.value.danceability?.toDouble() ?? 0,
|
||||||
tempo: value.min,
|
max: max.value.danceability?.toDouble() ?? 0,
|
||||||
);
|
),
|
||||||
max.value = max.value.copyWith(
|
onChanged: (value) {
|
||||||
tempo: value.max,
|
target.value = target.value.copyWith(
|
||||||
);
|
danceability: value.target,
|
||||||
},
|
);
|
||||||
),
|
min.value = min.value.copyWith(
|
||||||
RecommendationAttributeFields(
|
danceability: value.min,
|
||||||
title: Text(context.l10n.mode),
|
);
|
||||||
values: (
|
max.value = max.value.copyWith(
|
||||||
max: max.value.mode?.toDouble() ?? 0,
|
danceability: value.max,
|
||||||
target: target.value.mode?.toDouble() ?? 0,
|
);
|
||||||
min: min.value.mode?.toDouble() ?? 0,
|
},
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
RecommendationAttributeDials(
|
||||||
target.value = target.value.copyWith(
|
title: Text(context.l10n.energy),
|
||||||
mode: value.target,
|
values: (
|
||||||
);
|
target: target.value.energy?.toDouble() ?? 0,
|
||||||
min.value = min.value.copyWith(
|
min: min.value.energy?.toDouble() ?? 0,
|
||||||
mode: value.min,
|
max: max.value.energy?.toDouble() ?? 0,
|
||||||
);
|
),
|
||||||
max.value = max.value.copyWith(
|
onChanged: (value) {
|
||||||
mode: value.max,
|
target.value = target.value.copyWith(
|
||||||
);
|
energy: value.target,
|
||||||
},
|
);
|
||||||
),
|
min.value = min.value.copyWith(
|
||||||
RecommendationAttributeFields(
|
energy: value.min,
|
||||||
title: Text(context.l10n.time_signature),
|
);
|
||||||
values: (
|
max.value = max.value.copyWith(
|
||||||
max: max.value.timeSignature?.toDouble() ?? 0,
|
energy: value.max,
|
||||||
target: target.value.timeSignature?.toDouble() ?? 0,
|
);
|
||||||
min: min.value.timeSignature?.toDouble() ?? 0,
|
},
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
RecommendationAttributeDials(
|
||||||
target.value = target.value.copyWith(
|
title: Text(context.l10n.instrumentalness),
|
||||||
timeSignature: value.target,
|
values: (
|
||||||
);
|
target:
|
||||||
min.value = min.value.copyWith(
|
target.value.instrumentalness?.toDouble() ?? 0,
|
||||||
timeSignature: value.min,
|
min: min.value.instrumentalness?.toDouble() ?? 0,
|
||||||
);
|
max: max.value.instrumentalness?.toDouble() ?? 0,
|
||||||
max.value = max.value.copyWith(
|
),
|
||||||
timeSignature: value.max,
|
onChanged: (value) {
|
||||||
);
|
target.value = target.value.copyWith(
|
||||||
},
|
instrumentalness: value.target,
|
||||||
),
|
);
|
||||||
const Gap(20),
|
min.value = min.value.copyWith(
|
||||||
Center(
|
instrumentalness: value.min,
|
||||||
child: Button.primary(
|
);
|
||||||
leading: const Icon(SpotubeIcons.magic),
|
max.value = max.value.copyWith(
|
||||||
onPressed: artists.value.isEmpty &&
|
instrumentalness: value.max,
|
||||||
tracks.value.isEmpty &&
|
);
|
||||||
genres.value.isEmpty
|
},
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
final routeState =
|
|
||||||
GeneratePlaylistProviderInput(
|
|
||||||
seedArtists: artists.value
|
|
||||||
.map((a) => a.id!)
|
|
||||||
.toList(),
|
|
||||||
seedTracks:
|
|
||||||
tracks.value.map((t) => t.id!).toList(),
|
|
||||||
seedGenres: genres.value,
|
|
||||||
limit: limit.value,
|
|
||||||
max: max.value,
|
|
||||||
min: min.value,
|
|
||||||
target: target.value,
|
|
||||||
);
|
|
||||||
context.navigateTo(
|
|
||||||
PlaylistGenerateResultRoute(
|
|
||||||
state: routeState,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text(context.l10n.generate),
|
|
||||||
),
|
),
|
||||||
),
|
RecommendationAttributeDials(
|
||||||
],
|
title: Text(context.l10n.liveness),
|
||||||
),
|
values: (
|
||||||
);
|
target: target.value.liveness?.toDouble() ?? 0,
|
||||||
}),
|
min: min.value.liveness?.toDouble() ?? 0,
|
||||||
|
max: max.value.liveness?.toDouble() ?? 0,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
target.value = target.value.copyWith(
|
||||||
|
liveness: value.target,
|
||||||
|
);
|
||||||
|
min.value = min.value.copyWith(
|
||||||
|
liveness: value.min,
|
||||||
|
);
|
||||||
|
max.value = max.value.copyWith(
|
||||||
|
liveness: value.max,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RecommendationAttributeDials(
|
||||||
|
title: Text(context.l10n.loudness),
|
||||||
|
values: (
|
||||||
|
target: target.value.loudness?.toDouble() ?? 0,
|
||||||
|
min: min.value.loudness?.toDouble() ?? 0,
|
||||||
|
max: max.value.loudness?.toDouble() ?? 0,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
target.value = target.value.copyWith(
|
||||||
|
loudness: value.target,
|
||||||
|
);
|
||||||
|
min.value = min.value.copyWith(
|
||||||
|
loudness: value.min,
|
||||||
|
);
|
||||||
|
max.value = max.value.copyWith(
|
||||||
|
loudness: value.max,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RecommendationAttributeDials(
|
||||||
|
title: Text(context.l10n.speechiness),
|
||||||
|
values: (
|
||||||
|
target: target.value.speechiness?.toDouble() ?? 0,
|
||||||
|
min: min.value.speechiness?.toDouble() ?? 0,
|
||||||
|
max: max.value.speechiness?.toDouble() ?? 0,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
target.value = target.value.copyWith(
|
||||||
|
speechiness: value.target,
|
||||||
|
);
|
||||||
|
min.value = min.value.copyWith(
|
||||||
|
speechiness: value.min,
|
||||||
|
);
|
||||||
|
max.value = max.value.copyWith(
|
||||||
|
speechiness: value.max,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RecommendationAttributeDials(
|
||||||
|
title: Text(context.l10n.valence),
|
||||||
|
values: (
|
||||||
|
target: target.value.valence?.toDouble() ?? 0,
|
||||||
|
min: min.value.valence?.toDouble() ?? 0,
|
||||||
|
max: max.value.valence?.toDouble() ?? 0,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
target.value = target.value.copyWith(
|
||||||
|
valence: value.target,
|
||||||
|
);
|
||||||
|
min.value = min.value.copyWith(
|
||||||
|
valence: value.min,
|
||||||
|
);
|
||||||
|
max.value = max.value.copyWith(
|
||||||
|
valence: value.max,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RecommendationAttributeDials(
|
||||||
|
title: Text(context.l10n.popularity),
|
||||||
|
base: 100,
|
||||||
|
values: (
|
||||||
|
target: target.value.popularity?.toDouble() ?? 0,
|
||||||
|
min: min.value.popularity?.toDouble() ?? 0,
|
||||||
|
max: max.value.popularity?.toDouble() ?? 0,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
target.value = target.value.copyWith(
|
||||||
|
popularity: value.target,
|
||||||
|
);
|
||||||
|
min.value = min.value.copyWith(
|
||||||
|
popularity: value.min,
|
||||||
|
);
|
||||||
|
max.value = max.value.copyWith(
|
||||||
|
popularity: value.max,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RecommendationAttributeDials(
|
||||||
|
title: Text(context.l10n.key),
|
||||||
|
base: 11,
|
||||||
|
values: (
|
||||||
|
target: target.value.key?.toDouble() ?? 0,
|
||||||
|
min: min.value.key?.toDouble() ?? 0,
|
||||||
|
max: max.value.key?.toDouble() ?? 0,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
target.value = target.value.copyWith(
|
||||||
|
key: value.target,
|
||||||
|
);
|
||||||
|
min.value = min.value.copyWith(
|
||||||
|
key: value.min,
|
||||||
|
);
|
||||||
|
max.value = max.value.copyWith(
|
||||||
|
key: value.max,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RecommendationAttributeFields(
|
||||||
|
title: Text(context.l10n.duration),
|
||||||
|
values: (
|
||||||
|
max: (max.value.durationMs ?? 0) / 1000,
|
||||||
|
target: (target.value.durationMs ?? 0) / 1000,
|
||||||
|
min: (min.value.durationMs ?? 0) / 1000,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
target.value = target.value.copyWith(
|
||||||
|
durationMs: (value.target * 1000).toInt(),
|
||||||
|
);
|
||||||
|
min.value = min.value.copyWith(
|
||||||
|
durationMs: (value.min * 1000).toInt(),
|
||||||
|
);
|
||||||
|
max.value = max.value.copyWith(
|
||||||
|
durationMs: (value.max * 1000).toInt(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
presets: {
|
||||||
|
context.l10n.short: (min: 50, target: 90, max: 120),
|
||||||
|
context.l10n.medium: (
|
||||||
|
min: 120,
|
||||||
|
target: 180,
|
||||||
|
max: 200
|
||||||
|
),
|
||||||
|
context.l10n.long: (min: 480, target: 560, max: 640)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RecommendationAttributeFields(
|
||||||
|
title: Text(context.l10n.tempo),
|
||||||
|
values: (
|
||||||
|
max: max.value.tempo?.toDouble() ?? 0,
|
||||||
|
target: target.value.tempo?.toDouble() ?? 0,
|
||||||
|
min: min.value.tempo?.toDouble() ?? 0,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
target.value = target.value.copyWith(
|
||||||
|
tempo: value.target,
|
||||||
|
);
|
||||||
|
min.value = min.value.copyWith(
|
||||||
|
tempo: value.min,
|
||||||
|
);
|
||||||
|
max.value = max.value.copyWith(
|
||||||
|
tempo: value.max,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RecommendationAttributeFields(
|
||||||
|
title: Text(context.l10n.mode),
|
||||||
|
values: (
|
||||||
|
max: max.value.mode?.toDouble() ?? 0,
|
||||||
|
target: target.value.mode?.toDouble() ?? 0,
|
||||||
|
min: min.value.mode?.toDouble() ?? 0,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
target.value = target.value.copyWith(
|
||||||
|
mode: value.target,
|
||||||
|
);
|
||||||
|
min.value = min.value.copyWith(
|
||||||
|
mode: value.min,
|
||||||
|
);
|
||||||
|
max.value = max.value.copyWith(
|
||||||
|
mode: value.max,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RecommendationAttributeFields(
|
||||||
|
title: Text(context.l10n.time_signature),
|
||||||
|
values: (
|
||||||
|
max: max.value.timeSignature?.toDouble() ?? 0,
|
||||||
|
target: target.value.timeSignature?.toDouble() ?? 0,
|
||||||
|
min: min.value.timeSignature?.toDouble() ?? 0,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
target.value = target.value.copyWith(
|
||||||
|
timeSignature: value.target,
|
||||||
|
);
|
||||||
|
min.value = min.value.copyWith(
|
||||||
|
timeSignature: value.min,
|
||||||
|
);
|
||||||
|
max.value = max.value.copyWith(
|
||||||
|
timeSignature: value.max,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(20),
|
||||||
|
Center(
|
||||||
|
child: Button.primary(
|
||||||
|
leading: const Icon(SpotubeIcons.magic),
|
||||||
|
onPressed: artists.value.isEmpty &&
|
||||||
|
tracks.value.isEmpty &&
|
||||||
|
genres.value.isEmpty
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
final routeState =
|
||||||
|
GeneratePlaylistProviderInput(
|
||||||
|
seedArtists: artists.value
|
||||||
|
.map((a) => a.id!)
|
||||||
|
.toList(),
|
||||||
|
seedTracks: tracks.value
|
||||||
|
.map((t) => t.id!)
|
||||||
|
.toList(),
|
||||||
|
seedGenres: genres.value,
|
||||||
|
limit: limit.value,
|
||||||
|
max: max.value,
|
||||||
|
min: min.value,
|
||||||
|
target: target.value,
|
||||||
|
);
|
||||||
|
context.navigateTo(
|
||||||
|
PlaylistGenerateResultRoute(
|
||||||
|
state: routeState,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(context.l10n.generate),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -48,218 +48,225 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
|
|||||||
final isAllTrackSelected = selectedTracks.value.length ==
|
final isAllTrackSelected = selectedTracks.value.length ==
|
||||||
(generatedPlaylist.asData?.value.length ?? 0);
|
(generatedPlaylist.asData?.value.length ?? 0);
|
||||||
|
|
||||||
return Scaffold(
|
return SafeArea(
|
||||||
headers: const [
|
bottom: false,
|
||||||
TitleBar(leading: [BackButton()])
|
child: Scaffold(
|
||||||
],
|
headers: const [
|
||||||
child: generatedPlaylist.isLoading
|
TitleBar(leading: [BackButton()])
|
||||||
? Center(
|
],
|
||||||
child: Column(
|
child: generatedPlaylist.isLoading
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
? Center(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
child: Column(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
const CircularProgressIndicator(),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
Text(context.l10n.generating_playlist),
|
children: [
|
||||||
],
|
const CircularProgressIndicator(),
|
||||||
),
|
Text(context.l10n.generating_playlist),
|
||||||
)
|
],
|
||||||
: Padding(
|
),
|
||||||
padding: const EdgeInsets.all(8.0),
|
)
|
||||||
child: ListView(
|
: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.all(8.0),
|
||||||
GridView(
|
child: ListView(
|
||||||
gridDelegate:
|
children: [
|
||||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
GridView(
|
||||||
crossAxisCount: 2,
|
gridDelegate:
|
||||||
crossAxisSpacing: 8,
|
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
mainAxisSpacing: 8,
|
crossAxisCount: 2,
|
||||||
mainAxisExtent: 32,
|
crossAxisSpacing: 8,
|
||||||
),
|
mainAxisSpacing: 8,
|
||||||
shrinkWrap: true,
|
mainAxisExtent: 32,
|
||||||
children: [
|
|
||||||
Button.primary(
|
|
||||||
leading: const Icon(SpotubeIcons.play),
|
|
||||||
onPressed: selectedTracks.value.isEmpty
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
await playlistNotifier.load(
|
|
||||||
generatedPlaylist.asData!.value
|
|
||||||
.where(
|
|
||||||
(e) => selectedTracks.value
|
|
||||||
.contains(e.id!),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
autoPlay: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text(context.l10n.play),
|
|
||||||
),
|
),
|
||||||
Button.primary(
|
shrinkWrap: true,
|
||||||
leading: const Icon(SpotubeIcons.queueAdd),
|
children: [
|
||||||
onPressed: selectedTracks.value.isEmpty
|
Button.primary(
|
||||||
? null
|
leading: const Icon(SpotubeIcons.play),
|
||||||
: () async {
|
onPressed: selectedTracks.value.isEmpty
|
||||||
await playlistNotifier.addTracks(
|
? null
|
||||||
generatedPlaylist.asData!.value.where(
|
: () async {
|
||||||
(e) => selectedTracks.value.contains(e.id!),
|
await playlistNotifier.load(
|
||||||
),
|
generatedPlaylist.asData!.value
|
||||||
);
|
.where(
|
||||||
if (context.mounted) {
|
(e) => selectedTracks.value
|
||||||
showToast(
|
.contains(e.id!),
|
||||||
context: context,
|
|
||||||
location: ToastLocation.topRight,
|
|
||||||
builder: (context, overlay) {
|
|
||||||
return SurfaceCard(
|
|
||||||
child: Text(
|
|
||||||
context.l10n.add_count_to_queue(
|
|
||||||
selectedTracks.value.length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(context.l10n.add_to_queue),
|
|
||||||
),
|
|
||||||
Button.primary(
|
|
||||||
leading: const Icon(SpotubeIcons.addFilled),
|
|
||||||
onPressed: selectedTracks.value.isEmpty
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
final playlist = await showDialog<Playlist>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => PlaylistCreateDialog(
|
|
||||||
trackIds: selectedTracks.value,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (playlist != null && context.mounted) {
|
|
||||||
context.navigateTo(
|
|
||||||
PlaylistRoute(
|
|
||||||
id: playlist.id!,
|
|
||||||
playlist: playlist,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(context.l10n.create_a_playlist),
|
|
||||||
),
|
|
||||||
Button.primary(
|
|
||||||
leading: const Icon(SpotubeIcons.playlistAdd),
|
|
||||||
onPressed: selectedTracks.value.isEmpty
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
final hasAdded = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => PlaylistAddTrackDialog(
|
|
||||||
openFromPlaylist: null,
|
|
||||||
tracks: selectedTracks.value
|
|
||||||
.map(
|
|
||||||
(e) => generatedPlaylist.asData!.value
|
|
||||||
.firstWhere(
|
|
||||||
(element) => element.id == e,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
autoPlay: true,
|
||||||
);
|
|
||||||
|
|
||||||
if (context.mounted && hasAdded == true) {
|
|
||||||
showToast(
|
|
||||||
context: context,
|
|
||||||
location: ToastLocation.topRight,
|
|
||||||
builder: (context, overlay) {
|
|
||||||
return SurfaceCard(
|
|
||||||
child: Text(
|
|
||||||
context.l10n.add_count_to_playlist(
|
|
||||||
selectedTracks.value.length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
},
|
child: Text(context.l10n.play),
|
||||||
child: Text(context.l10n.add_to_playlist),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
if (generatedPlaylist.asData?.value != null)
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
context.l10n.selected_count_tracks(
|
|
||||||
selectedTracks.value.length,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Button.secondary(
|
Button.primary(
|
||||||
onPressed: () {
|
leading: const Icon(SpotubeIcons.queueAdd),
|
||||||
if (isAllTrackSelected) {
|
onPressed: selectedTracks.value.isEmpty
|
||||||
selectedTracks.value = [];
|
? null
|
||||||
} else {
|
: () async {
|
||||||
selectedTracks.value = generatedPlaylist
|
await playlistNotifier.addTracks(
|
||||||
.asData?.value
|
generatedPlaylist.asData!.value.where(
|
||||||
.map((e) => e.id!)
|
(e) =>
|
||||||
.toList() ??
|
selectedTracks.value.contains(e.id!),
|
||||||
[];
|
),
|
||||||
}
|
);
|
||||||
},
|
if (context.mounted) {
|
||||||
leading: const Icon(SpotubeIcons.selectionCheck),
|
showToast(
|
||||||
child: Text(
|
context: context,
|
||||||
isAllTrackSelected
|
location: ToastLocation.topRight,
|
||||||
? context.l10n.deselect_all
|
builder: (context, overlay) {
|
||||||
: context.l10n.select_all,
|
return SurfaceCard(
|
||||||
),
|
child: Text(
|
||||||
|
context.l10n.add_count_to_queue(
|
||||||
|
selectedTracks.value.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(context.l10n.add_to_queue),
|
||||||
),
|
),
|
||||||
|
Button.primary(
|
||||||
|
leading: const Icon(SpotubeIcons.addFilled),
|
||||||
|
onPressed: selectedTracks.value.isEmpty
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
final playlist = await showDialog<Playlist>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PlaylistCreateDialog(
|
||||||
|
trackIds: selectedTracks.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (playlist != null && context.mounted) {
|
||||||
|
context.navigateTo(
|
||||||
|
PlaylistRoute(
|
||||||
|
id: playlist.id!,
|
||||||
|
playlist: playlist,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(context.l10n.create_a_playlist),
|
||||||
|
),
|
||||||
|
Button.primary(
|
||||||
|
leading: const Icon(SpotubeIcons.playlistAdd),
|
||||||
|
onPressed: selectedTracks.value.isEmpty
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
final hasAdded = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
PlaylistAddTrackDialog(
|
||||||
|
openFromPlaylist: null,
|
||||||
|
tracks: selectedTracks.value
|
||||||
|
.map(
|
||||||
|
(e) => generatedPlaylist
|
||||||
|
.asData!.value
|
||||||
|
.firstWhere(
|
||||||
|
(element) => element.id == e,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted && hasAdded == true) {
|
||||||
|
showToast(
|
||||||
|
context: context,
|
||||||
|
location: ToastLocation.topRight,
|
||||||
|
builder: (context, overlay) {
|
||||||
|
return SurfaceCard(
|
||||||
|
child: Text(
|
||||||
|
context.l10n.add_count_to_playlist(
|
||||||
|
selectedTracks.value.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(context.l10n.add_to_playlist),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 16),
|
||||||
SafeArea(
|
if (generatedPlaylist.asData?.value != null)
|
||||||
child: Column(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
for (final track
|
Text(
|
||||||
in generatedPlaylist.asData?.value ?? [])
|
context.l10n.selected_count_tracks(
|
||||||
Row(
|
selectedTracks.value.length,
|
||||||
spacing: 5,
|
),
|
||||||
children: [
|
),
|
||||||
Checkbox(
|
Button.secondary(
|
||||||
state: selectedTracks.value.contains(track.id)
|
onPressed: () {
|
||||||
? CheckboxState.checked
|
if (isAllTrackSelected) {
|
||||||
: CheckboxState.unchecked,
|
selectedTracks.value = [];
|
||||||
onChanged: (value) {
|
} else {
|
||||||
if (value == CheckboxState.checked) {
|
selectedTracks.value = generatedPlaylist
|
||||||
selectedTracks.value.add(track.id!);
|
.asData?.value
|
||||||
} else {
|
.map((e) => e.id!)
|
||||||
selectedTracks.value.remove(track.id);
|
.toList() ??
|
||||||
}
|
[];
|
||||||
selectedTracks.value =
|
}
|
||||||
selectedTracks.value.toList();
|
},
|
||||||
},
|
leading: const Icon(SpotubeIcons.selectionCheck),
|
||||||
),
|
child: Text(
|
||||||
Expanded(
|
isAllTrackSelected
|
||||||
child: GestureDetector(
|
? context.l10n.deselect_all
|
||||||
onTap: () {
|
: context.l10n.select_all,
|
||||||
selectedTracks.value.contains(track.id)
|
),
|
||||||
? selectedTracks.value.remove(track.id)
|
),
|
||||||
: selectedTracks.value.add(track.id!);
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
for (final track
|
||||||
|
in generatedPlaylist.asData?.value ?? [])
|
||||||
|
Row(
|
||||||
|
spacing: 5,
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
state: selectedTracks.value.contains(track.id)
|
||||||
|
? CheckboxState.checked
|
||||||
|
: CheckboxState.unchecked,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == CheckboxState.checked) {
|
||||||
|
selectedTracks.value.add(track.id!);
|
||||||
|
} else {
|
||||||
|
selectedTracks.value.remove(track.id);
|
||||||
|
}
|
||||||
selectedTracks.value =
|
selectedTracks.value =
|
||||||
selectedTracks.value.toList();
|
selectedTracks.value.toList();
|
||||||
},
|
},
|
||||||
child: SimpleTrackTile(track: track),
|
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
],
|
child: GestureDetector(
|
||||||
)
|
onTap: () {
|
||||||
],
|
selectedTracks.value.contains(track.id)
|
||||||
|
? selectedTracks.value
|
||||||
|
.remove(track.id)
|
||||||
|
: selectedTracks.value.add(track.id!);
|
||||||
|
selectedTracks.value =
|
||||||
|
selectedTracks.value.toList();
|
||||||
|
},
|
||||||
|
child: SimpleTrackTile(track: track),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,50 +27,53 @@ class WebViewLoginPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return SafeArea(
|
||||||
headers: const [
|
bottom: false,
|
||||||
TitleBar(
|
child: Scaffold(
|
||||||
leading: [BackButton(color: Colors.white)],
|
headers: const [
|
||||||
backgroundColor: Colors.transparent,
|
TitleBar(
|
||||||
),
|
leading: [BackButton(color: Colors.white)],
|
||||||
],
|
backgroundColor: Colors.transparent,
|
||||||
floatingHeader: true,
|
),
|
||||||
child: InAppWebView(
|
],
|
||||||
initialSettings: InAppWebViewSettings(
|
floatingHeader: true,
|
||||||
userAgent:
|
child: InAppWebView(
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 safari/537.36",
|
initialSettings: InAppWebViewSettings(
|
||||||
),
|
userAgent:
|
||||||
initialUrlRequest: URLRequest(
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 safari/537.36",
|
||||||
url: WebUri("https://accounts.spotify.com/"),
|
),
|
||||||
),
|
initialUrlRequest: URLRequest(
|
||||||
onPermissionRequest: (controller, permissionRequest) async {
|
url: WebUri("https://accounts.spotify.com/"),
|
||||||
return PermissionResponse(
|
),
|
||||||
resources: permissionRequest.resources,
|
onPermissionRequest: (controller, permissionRequest) async {
|
||||||
action: PermissionResponseAction.GRANT,
|
return PermissionResponse(
|
||||||
);
|
resources: permissionRequest.resources,
|
||||||
},
|
action: PermissionResponseAction.GRANT,
|
||||||
onLoadStop: (controller, action) async {
|
);
|
||||||
if (action == null) return;
|
},
|
||||||
String url = action.toString();
|
onLoadStop: (controller, action) async {
|
||||||
if (url.endsWith("/")) {
|
if (action == null) return;
|
||||||
url = url.substring(0, url.length - 1);
|
String url = action.toString();
|
||||||
}
|
if (url.endsWith("/")) {
|
||||||
|
url = url.substring(0, url.length - 1);
|
||||||
final exp = RegExp(r"https:\/\/accounts.spotify.com\/.+\/status");
|
|
||||||
|
|
||||||
if (exp.hasMatch(url)) {
|
|
||||||
final cookies =
|
|
||||||
await CookieManager.instance().getCookies(url: action);
|
|
||||||
final cookieHeader =
|
|
||||||
"sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}";
|
|
||||||
|
|
||||||
await authenticationNotifier.login(cookieHeader);
|
|
||||||
if (context.mounted) {
|
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
context.navigateTo(const HomeRoute());
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
final exp = RegExp(r"https:\/\/accounts.spotify.com\/.+\/status");
|
||||||
|
|
||||||
|
if (exp.hasMatch(url)) {
|
||||||
|
final cookies =
|
||||||
|
await CookieManager.instance().getCookies(url: action);
|
||||||
|
final cookieHeader =
|
||||||
|
"sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}";
|
||||||
|
|
||||||
|
await authenticationNotifier.login(cookieHeader);
|
||||||
|
if (context.mounted) {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
context.navigateTo(const HomeRoute());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,6 @@ class ProfilePage extends HookConsumerWidget {
|
|||||||
headers: [
|
headers: [
|
||||||
TitleBar(
|
TitleBar(
|
||||||
title: Text(context.l10n.profile),
|
title: Text(context.l10n.profile),
|
||||||
automaticallyImplyLeading: true,
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
child: Skeletonizer(
|
child: Skeletonizer(
|
||||||
|
@ -31,163 +31,166 @@ class AboutSpotubePage extends HookConsumerWidget {
|
|||||||
|
|
||||||
const colon = TableCell(child: Text(":"));
|
const colon = TableCell(child: Text(":"));
|
||||||
|
|
||||||
return Scaffold(
|
return SafeArea(
|
||||||
headers: [
|
bottom: false,
|
||||||
TitleBar(
|
child: Scaffold(
|
||||||
leading: const [BackButton()],
|
headers: [
|
||||||
title: Text(context.l10n.about_spotube),
|
TitleBar(
|
||||||
)
|
leading: const [BackButton()],
|
||||||
],
|
title: Text(context.l10n.about_spotube),
|
||||||
child: SingleChildScrollView(
|
)
|
||||||
child: Padding(
|
],
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
Assets.spotubeLogoPng.image(
|
child: Column(
|
||||||
height: 200,
|
children: [
|
||||||
width: 200,
|
Assets.spotubeLogoPng.image(
|
||||||
),
|
height: 200,
|
||||||
Center(
|
width: 200,
|
||||||
child: Column(
|
),
|
||||||
children: [
|
Center(
|
||||||
Text(context.l10n.spotube_description).semiBold().large(),
|
child: Column(
|
||||||
const SizedBox(height: 20),
|
children: [
|
||||||
Table(
|
Text(context.l10n.spotube_description).semiBold().large(),
|
||||||
columnWidths: const {
|
const SizedBox(height: 20),
|
||||||
0: FixedTableSize(95),
|
Table(
|
||||||
1: FixedTableSize(10),
|
columnWidths: const {
|
||||||
2: IntrinsicTableSize(),
|
0: FixedTableSize(95),
|
||||||
},
|
1: FixedTableSize(10),
|
||||||
defaultRowHeight: const FixedTableSize(40),
|
2: IntrinsicTableSize(),
|
||||||
rows: [
|
},
|
||||||
TableRow(
|
defaultRowHeight: const FixedTableSize(40),
|
||||||
cells: [
|
rows: [
|
||||||
TableCell(child: Text(context.l10n.founder)),
|
TableRow(
|
||||||
colon,
|
cells: [
|
||||||
TableCell(
|
TableCell(child: Text(context.l10n.founder)),
|
||||||
child: Hyperlink(
|
colon,
|
||||||
context.l10n.kingkor_roy_tirtho,
|
TableCell(
|
||||||
"https://github.com/KRTirtho",
|
child: Hyperlink(
|
||||||
|
context.l10n.kingkor_roy_tirtho,
|
||||||
|
"https://github.com/KRTirtho",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TableRow(
|
||||||
|
cells: [
|
||||||
|
TableCell(child: Text(context.l10n.version)),
|
||||||
|
colon,
|
||||||
|
TableCell(child: Text("v${packageInfo.version}"))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TableRow(
|
||||||
|
cells: [
|
||||||
|
TableCell(child: Text(context.l10n.channel)),
|
||||||
|
colon,
|
||||||
|
TableCell(child: Text(Env.releaseChannel.name))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TableRow(
|
||||||
|
cells: [
|
||||||
|
TableCell(child: Text(context.l10n.build_number)),
|
||||||
|
colon,
|
||||||
|
TableCell(
|
||||||
|
child: Text(packageInfo.buildNumber
|
||||||
|
.replaceAll(".", " ")),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TableRow(
|
||||||
|
cells: [
|
||||||
|
TableCell(child: Text(context.l10n.repository)),
|
||||||
|
colon,
|
||||||
|
const TableCell(
|
||||||
|
child: Hyperlink(
|
||||||
|
"github.com/KRTirtho/spotube",
|
||||||
|
"https://github.com/KRTirtho/spotube",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
],
|
||||||
],
|
),
|
||||||
),
|
TableRow(
|
||||||
TableRow(
|
cells: [
|
||||||
cells: [
|
TableCell(child: Text(context.l10n.license)),
|
||||||
TableCell(child: Text(context.l10n.version)),
|
colon,
|
||||||
colon,
|
const TableCell(
|
||||||
TableCell(child: Text("v${packageInfo.version}"))
|
child: Hyperlink(
|
||||||
],
|
"BSD-4-Clause",
|
||||||
),
|
"https://raw.githubusercontent.com/KRTirtho/spotube/master/LICENSE",
|
||||||
TableRow(
|
),
|
||||||
cells: [
|
|
||||||
TableCell(child: Text(context.l10n.channel)),
|
|
||||||
colon,
|
|
||||||
TableCell(child: Text(Env.releaseChannel.name))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
TableRow(
|
|
||||||
cells: [
|
|
||||||
TableCell(child: Text(context.l10n.build_number)),
|
|
||||||
colon,
|
|
||||||
TableCell(
|
|
||||||
child: Text(
|
|
||||||
packageInfo.buildNumber.replaceAll(".", " ")),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
TableRow(
|
|
||||||
cells: [
|
|
||||||
TableCell(child: Text(context.l10n.repository)),
|
|
||||||
colon,
|
|
||||||
const TableCell(
|
|
||||||
child: Hyperlink(
|
|
||||||
"github.com/KRTirtho/spotube",
|
|
||||||
"https://github.com/KRTirtho/spotube",
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
TableRow(
|
||||||
TableRow(
|
cells: [
|
||||||
cells: [
|
TableCell(child: Text(context.l10n.bug_issues)),
|
||||||
TableCell(child: Text(context.l10n.license)),
|
colon,
|
||||||
colon,
|
const TableCell(
|
||||||
const TableCell(
|
child: Hyperlink(
|
||||||
child: Hyperlink(
|
"github.com/KRTirtho/spotube/issues",
|
||||||
"BSD-4-Clause",
|
"https://github.com/KRTirtho/spotube/issues",
|
||||||
"https://raw.githubusercontent.com/KRTirtho/spotube/master/LICENSE",
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
TableRow(
|
),
|
||||||
cells: [
|
],
|
||||||
TableCell(child: Text(context.l10n.bug_issues)),
|
),
|
||||||
colon,
|
),
|
||||||
const TableCell(
|
const SizedBox(height: 20),
|
||||||
child: Hyperlink(
|
MouseRegion(
|
||||||
"github.com/KRTirtho/spotube/issues",
|
cursor: SystemMouseCursors.click,
|
||||||
"https://github.com/KRTirtho/spotube/issues",
|
child: GestureDetector(
|
||||||
),
|
onTap: () => launchUrl(
|
||||||
),
|
Uri.parse("https://discord.gg/uJ94vxB6vg"),
|
||||||
],
|
mode: LaunchMode.externalApplication,
|
||||||
),
|
),
|
||||||
],
|
child: const UniversalImage(
|
||||||
|
path:
|
||||||
|
"https://discord.com/api/guilds/1012234096237350943/widget.png?style=banner2",
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
MouseRegion(
|
|
||||||
cursor: SystemMouseCursors.click,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => launchUrl(
|
|
||||||
Uri.parse("https://discord.gg/uJ94vxB6vg"),
|
|
||||||
mode: LaunchMode.externalApplication,
|
|
||||||
),
|
|
||||||
child: const UniversalImage(
|
|
||||||
path:
|
|
||||||
"https://discord.com/api/guilds/1012234096237350943/widget.png?style=banner2",
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 20),
|
||||||
const SizedBox(height: 20),
|
Text(
|
||||||
Text(
|
context.l10n.made_with,
|
||||||
context.l10n.made_with,
|
textAlign: TextAlign.center,
|
||||||
textAlign: TextAlign.center,
|
style: theme.typography.small,
|
||||||
style: theme.typography.small,
|
),
|
||||||
),
|
Text(
|
||||||
Text(
|
context.l10n.copyright(DateTime.now().year),
|
||||||
context.l10n.copyright(DateTime.now().year),
|
textAlign: TextAlign.center,
|
||||||
textAlign: TextAlign.center,
|
style: theme.typography.small,
|
||||||
style: theme.typography.small,
|
),
|
||||||
),
|
const SizedBox(height: 20),
|
||||||
const SizedBox(height: 20),
|
ConstrainedBox(
|
||||||
ConstrainedBox(
|
constraints: const BoxConstraints(maxWidth: 750),
|
||||||
constraints: const BoxConstraints(maxWidth: 750),
|
child: SafeArea(
|
||||||
child: SafeArea(
|
child: license.when(
|
||||||
child: license.when(
|
data: (data) {
|
||||||
data: (data) {
|
return Text(
|
||||||
return Text(
|
data,
|
||||||
data,
|
style: theme.typography.small,
|
||||||
style: theme.typography.small,
|
);
|
||||||
);
|
},
|
||||||
},
|
loading: () {
|
||||||
loading: () {
|
return const Center(
|
||||||
return const Center(
|
child: CircularProgressIndicator(),
|
||||||
child: CircularProgressIndicator(),
|
);
|
||||||
);
|
},
|
||||||
},
|
error: (e, s) {
|
||||||
error: (e, s) {
|
return Text(
|
||||||
return Text(
|
e.toString(),
|
||||||
e.toString(),
|
style: theme.typography.small,
|
||||||
style: theme.typography.small,
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -47,50 +47,52 @@ class BlackListPage extends HookConsumerWidget {
|
|||||||
[blacklist, searchText.value],
|
[blacklist, searchText.value],
|
||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return SafeArea(
|
||||||
headers: [
|
bottom: false,
|
||||||
TitleBar(
|
child: Scaffold(
|
||||||
title: Text(context.l10n.blacklist),
|
headers: [
|
||||||
leading: const [BackButton()],
|
TitleBar(
|
||||||
)
|
title: Text(context.l10n.blacklist),
|
||||||
],
|
leading: const [BackButton()],
|
||||||
child: Column(
|
)
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: TextField(
|
|
||||||
onChanged: (value) => searchText.value = value,
|
|
||||||
placeholder: Text(context.l10n.search),
|
|
||||||
leading: const Icon(SpotubeIcons.search),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
InterScrollbar(
|
|
||||||
controller: controller,
|
|
||||||
child: ListView.builder(
|
|
||||||
controller: controller,
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: filteredBlacklist.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final item = filteredBlacklist.elementAt(index);
|
|
||||||
return ButtonTile(
|
|
||||||
style: ButtonVariance.ghost,
|
|
||||||
leading: Text("${index + 1}."),
|
|
||||||
title: Text("${item.name} (${item.elementType.name})"),
|
|
||||||
subtitle: Text(item.elementId),
|
|
||||||
trailing: IconButton.ghost(
|
|
||||||
icon: Icon(SpotubeIcons.trash, color: Colors.red[400]),
|
|
||||||
onPressed: () {
|
|
||||||
ref
|
|
||||||
.read(blacklistProvider.notifier)
|
|
||||||
.remove(filteredBlacklist.elementAt(index).elementId);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: TextField(
|
||||||
|
onChanged: (value) => searchText.value = value,
|
||||||
|
placeholder: Text(context.l10n.search),
|
||||||
|
leading: const Icon(SpotubeIcons.search),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
InterScrollbar(
|
||||||
|
controller: controller,
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: controller,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: filteredBlacklist.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = filteredBlacklist.elementAt(index);
|
||||||
|
return ButtonTile(
|
||||||
|
style: ButtonVariance.ghost,
|
||||||
|
leading: Text("${index + 1}."),
|
||||||
|
title: Text("${item.name} (${item.elementType.name})"),
|
||||||
|
subtitle: Text(item.elementId),
|
||||||
|
trailing: IconButton.ghost(
|
||||||
|
icon: Icon(SpotubeIcons.trash, color: Colors.red[400]),
|
||||||
|
onPressed: () {
|
||||||
|
ref.read(blacklistProvider.notifier).remove(
|
||||||
|
filteredBlacklist.elementAt(index).elementId);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,6 @@ class SettingsPage extends HookConsumerWidget {
|
|||||||
headers: [
|
headers: [
|
||||||
TitleBar(
|
TitleBar(
|
||||||
title: Text(context.l10n.settings),
|
title: Text(context.l10n.settings),
|
||||||
automaticallyImplyLeading: true,
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
child: Scrollbar(
|
child: Scrollbar(
|
||||||
|
@ -26,31 +26,33 @@ class StatsAlbumsPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final albumsData = topAlbums.asData?.value.items ?? [];
|
final albumsData = topAlbums.asData?.value.items ?? [];
|
||||||
|
|
||||||
return Scaffold(
|
return SafeArea(
|
||||||
headers: [
|
bottom: false,
|
||||||
TitleBar(
|
child: Scaffold(
|
||||||
automaticallyImplyLeading: true,
|
headers: [
|
||||||
title: Text(context.l10n.albums),
|
TitleBar(
|
||||||
)
|
title: Text(context.l10n.albums),
|
||||||
],
|
)
|
||||||
child: Skeletonizer(
|
],
|
||||||
enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
|
child: Skeletonizer(
|
||||||
child: InfiniteList(
|
enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
|
||||||
onFetchData: () async {
|
child: InfiniteList(
|
||||||
await topAlbumsNotifier.fetchMore();
|
onFetchData: () async {
|
||||||
},
|
await topAlbumsNotifier.fetchMore();
|
||||||
hasError: topAlbums.hasError,
|
},
|
||||||
isLoading: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
|
hasError: topAlbums.hasError,
|
||||||
hasReachedMax: topAlbums.asData?.value.hasMore ?? true,
|
isLoading: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
|
||||||
itemCount: albumsData.length,
|
hasReachedMax: topAlbums.asData?.value.hasMore ?? true,
|
||||||
itemBuilder: (context, index) {
|
itemCount: albumsData.length,
|
||||||
final album = albumsData[index];
|
itemBuilder: (context, index) {
|
||||||
return StatsAlbumItem(
|
final album = albumsData[index];
|
||||||
album: album.album,
|
return StatsAlbumItem(
|
||||||
info: Text(context.l10n
|
album: album.album,
|
||||||
.count_plays(compactNumberFormatter.format(album.count))),
|
info: Text(context.l10n
|
||||||
);
|
.count_plays(compactNumberFormatter.format(album.count))),
|
||||||
},
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -29,31 +29,33 @@ class StatsArtistsPage extends HookConsumerWidget {
|
|||||||
final artistsData = useMemoized(
|
final artistsData = useMemoized(
|
||||||
() => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]);
|
() => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]);
|
||||||
|
|
||||||
return Scaffold(
|
return SafeArea(
|
||||||
headers: [
|
bottom: false,
|
||||||
TitleBar(
|
child: Scaffold(
|
||||||
automaticallyImplyLeading: true,
|
headers: [
|
||||||
title: Text(context.l10n.artists),
|
TitleBar(
|
||||||
)
|
title: Text(context.l10n.artists),
|
||||||
],
|
)
|
||||||
child: Skeletonizer(
|
],
|
||||||
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
child: Skeletonizer(
|
||||||
child: InfiniteList(
|
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||||
onFetchData: () async {
|
child: InfiniteList(
|
||||||
await topTracksNotifier.fetchMore();
|
onFetchData: () async {
|
||||||
},
|
await topTracksNotifier.fetchMore();
|
||||||
hasError: topTracks.hasError,
|
},
|
||||||
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
hasError: topTracks.hasError,
|
||||||
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
|
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||||
itemCount: artistsData.length,
|
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
|
||||||
itemBuilder: (context, index) {
|
itemCount: artistsData.length,
|
||||||
final artist = artistsData[index];
|
itemBuilder: (context, index) {
|
||||||
return StatsArtistItem(
|
final artist = artistsData[index];
|
||||||
artist: artist.artist,
|
return StatsArtistItem(
|
||||||
info: Text(context.l10n
|
artist: artist.artist,
|
||||||
.count_plays(compactNumberFormatter.format(artist.count))),
|
info: Text(context.l10n
|
||||||
);
|
.count_plays(compactNumberFormatter.format(artist.count))),
|
||||||
},
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -50,79 +50,83 @@ class StatsStreamFeesPage extends HookConsumerWidget {
|
|||||||
HistoryDuration.allTime: context.l10n.all_time,
|
HistoryDuration.allTime: context.l10n.all_time,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Scaffold(
|
return SafeArea(
|
||||||
headers: [
|
bottom: false,
|
||||||
TitleBar(
|
child: Scaffold(
|
||||||
automaticallyImplyLeading: true,
|
headers: [
|
||||||
title: Text(context.l10n.streaming_fees_hypothetical),
|
TitleBar(
|
||||||
)
|
title: Text(context.l10n.streaming_fees_hypothetical),
|
||||||
],
|
)
|
||||||
child: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverCrossAxisConstrained(
|
|
||||||
maxCrossAxisExtent: 600,
|
|
||||||
alignment: -1,
|
|
||||||
child: SliverPadding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
sliver: SliverToBoxAdapter(
|
|
||||||
child: Text(
|
|
||||||
context.l10n.spotify_hipotetical_calculation,
|
|
||||||
).small().muted(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
context.l10n.total_money(usdFormatter.format(total)),
|
|
||||||
).semiBold().large(),
|
|
||||||
Select<HistoryDuration>(
|
|
||||||
value: duration.value,
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value == null) return;
|
|
||||||
duration.value = value;
|
|
||||||
},
|
|
||||||
itemBuilder: (context, value) => Text(translations[value]!),
|
|
||||||
constraints: const BoxConstraints(maxWidth: 150),
|
|
||||||
popupWidthConstraint: PopoverConstraint.anchorMaxSize,
|
|
||||||
children: [
|
|
||||||
for (final entry in translations.entries)
|
|
||||||
SelectItemButton(
|
|
||||||
value: entry.key,
|
|
||||||
child: Text(entry.value),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverSafeArea(
|
|
||||||
sliver: Skeletonizer.sliver(
|
|
||||||
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
|
||||||
child: SliverInfiniteList(
|
|
||||||
onFetchData: () async {
|
|
||||||
await topTracksNotifier.fetchMore();
|
|
||||||
},
|
|
||||||
hasError: topTracks.hasError,
|
|
||||||
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
|
||||||
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
|
|
||||||
itemCount: artistsData.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final artist = artistsData[index];
|
|
||||||
return StatsArtistItem(
|
|
||||||
artist: artist.artist,
|
|
||||||
info: Text(usdFormatter.format(artist.count * 0.005)),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverCrossAxisConstrained(
|
||||||
|
maxCrossAxisExtent: 600,
|
||||||
|
alignment: -1,
|
||||||
|
child: SliverPadding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: Text(
|
||||||
|
context.l10n.spotify_hipotetical_calculation,
|
||||||
|
).small().muted(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.l10n.total_money(usdFormatter.format(total)),
|
||||||
|
).semiBold().large(),
|
||||||
|
Select<HistoryDuration>(
|
||||||
|
value: duration.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
duration.value = value;
|
||||||
|
},
|
||||||
|
itemBuilder: (context, value) =>
|
||||||
|
Text(translations[value]!),
|
||||||
|
constraints: const BoxConstraints(maxWidth: 150),
|
||||||
|
popupWidthConstraint: PopoverConstraint.anchorMaxSize,
|
||||||
|
children: [
|
||||||
|
for (final entry in translations.entries)
|
||||||
|
SelectItemButton(
|
||||||
|
value: entry.key,
|
||||||
|
child: Text(entry.value),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverSafeArea(
|
||||||
|
sliver: Skeletonizer.sliver(
|
||||||
|
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||||
|
child: SliverInfiniteList(
|
||||||
|
onFetchData: () async {
|
||||||
|
await topTracksNotifier.fetchMore();
|
||||||
|
},
|
||||||
|
hasError: topTracks.hasError,
|
||||||
|
isLoading:
|
||||||
|
topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||||
|
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
|
||||||
|
itemCount: artistsData.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final artist = artistsData[index];
|
||||||
|
return StatsArtistItem(
|
||||||
|
artist: artist.artist,
|
||||||
|
info: Text(usdFormatter.format(artist.count * 0.005)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -28,34 +28,36 @@ class StatsMinutesPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final tracksData = topTracks.asData?.value.items ?? [];
|
final tracksData = topTracks.asData?.value.items ?? [];
|
||||||
|
|
||||||
return Scaffold(
|
return SafeArea(
|
||||||
headers: [
|
bottom: false,
|
||||||
TitleBar(
|
child: Scaffold(
|
||||||
title: Text(context.l10n.minutes_listened),
|
headers: [
|
||||||
automaticallyImplyLeading: true,
|
TitleBar(
|
||||||
)
|
title: Text(context.l10n.minutes_listened),
|
||||||
],
|
)
|
||||||
child: Skeletonizer(
|
],
|
||||||
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
child: Skeletonizer(
|
||||||
child: InfiniteList(
|
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||||
separatorBuilder: (context, index) => const Gap(8),
|
child: InfiniteList(
|
||||||
onFetchData: () async {
|
separatorBuilder: (context, index) => const Gap(8),
|
||||||
await topTracksNotifier.fetchMore();
|
onFetchData: () async {
|
||||||
},
|
await topTracksNotifier.fetchMore();
|
||||||
hasError: topTracks.hasError,
|
},
|
||||||
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
hasError: topTracks.hasError,
|
||||||
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
|
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||||
itemCount: tracksData.length,
|
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
|
||||||
itemBuilder: (context, index) {
|
itemCount: tracksData.length,
|
||||||
final track = tracksData[index];
|
itemBuilder: (context, index) {
|
||||||
return StatsTrackItem(
|
final track = tracksData[index];
|
||||||
track: track.track,
|
return StatsTrackItem(
|
||||||
info: Text(
|
track: track.track,
|
||||||
context.l10n.count_mins(compactNumberFormatter
|
info: Text(
|
||||||
.format(track.count * track.track.duration!.inMinutes)),
|
context.l10n.count_mins(compactNumberFormatter
|
||||||
),
|
.format(track.count * track.track.duration!.inMinutes)),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -27,33 +27,36 @@ class StatsPlaylistsPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final playlistsData = topPlaylists.asData?.value.items ?? [];
|
final playlistsData = topPlaylists.asData?.value.items ?? [];
|
||||||
|
|
||||||
return Scaffold(
|
return SafeArea(
|
||||||
headers: [
|
bottom: false,
|
||||||
TitleBar(
|
child: Scaffold(
|
||||||
automaticallyImplyLeading: true,
|
headers: [
|
||||||
title: Text(context.l10n.playlists),
|
TitleBar(
|
||||||
)
|
title: Text(context.l10n.playlists),
|
||||||
],
|
)
|
||||||
child: Skeletonizer(
|
],
|
||||||
enabled: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage,
|
child: Skeletonizer(
|
||||||
child: InfiniteList(
|
enabled: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage,
|
||||||
onFetchData: () async {
|
child: InfiniteList(
|
||||||
await topPlaylistsNotifier.fetchMore();
|
onFetchData: () async {
|
||||||
},
|
await topPlaylistsNotifier.fetchMore();
|
||||||
hasError: topPlaylists.hasError,
|
},
|
||||||
isLoading: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage,
|
hasError: topPlaylists.hasError,
|
||||||
hasReachedMax: topPlaylists.asData?.value.hasMore ?? true,
|
isLoading:
|
||||||
itemCount: playlistsData.length,
|
topPlaylists.isLoading && !topPlaylists.isLoadingNextPage,
|
||||||
itemBuilder: (context, index) {
|
hasReachedMax: topPlaylists.asData?.value.hasMore ?? true,
|
||||||
final playlist = playlistsData[index];
|
itemCount: playlistsData.length,
|
||||||
return StatsPlaylistItem(
|
itemBuilder: (context, index) {
|
||||||
playlist: playlist.playlist,
|
final playlist = playlistsData[index];
|
||||||
info: Text(
|
return StatsPlaylistItem(
|
||||||
context.l10n
|
playlist: playlist.playlist,
|
||||||
.count_plays(compactNumberFormatter.format(playlist.count)),
|
info: Text(
|
||||||
),
|
context.l10n.count_plays(
|
||||||
);
|
compactNumberFormatter.format(playlist.count)),
|
||||||
},
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -28,34 +28,36 @@ class StatsStreamsPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final tracksData = topTracks.asData?.value.items ?? [];
|
final tracksData = topTracks.asData?.value.items ?? [];
|
||||||
|
|
||||||
return Scaffold(
|
return SafeArea(
|
||||||
headers: [
|
bottom: false,
|
||||||
TitleBar(
|
child: Scaffold(
|
||||||
title: Text(context.l10n.streamed_songs),
|
headers: [
|
||||||
automaticallyImplyLeading: true,
|
TitleBar(
|
||||||
)
|
title: Text(context.l10n.streamed_songs),
|
||||||
],
|
)
|
||||||
child: Skeletonizer(
|
],
|
||||||
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
child: Skeletonizer(
|
||||||
child: InfiniteList(
|
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||||
separatorBuilder: (context, index) => const Gap(8),
|
child: InfiniteList(
|
||||||
onFetchData: () async {
|
separatorBuilder: (context, index) => const Gap(8),
|
||||||
await topTracksNotifier.fetchMore();
|
onFetchData: () async {
|
||||||
},
|
await topTracksNotifier.fetchMore();
|
||||||
hasError: topTracks.hasError,
|
},
|
||||||
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
hasError: topTracks.hasError,
|
||||||
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
|
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||||
itemCount: tracksData.length,
|
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
|
||||||
itemBuilder: (context, index) {
|
itemCount: tracksData.length,
|
||||||
final track = tracksData[index];
|
itemBuilder: (context, index) {
|
||||||
return StatsTrackItem(
|
final track = tracksData[index];
|
||||||
track: track.track,
|
return StatsTrackItem(
|
||||||
info: Text(
|
track: track.track,
|
||||||
context.l10n
|
info: Text(
|
||||||
.count_plays(compactNumberFormatter.format(track.count)),
|
context.l10n
|
||||||
),
|
.count_plays(compactNumberFormatter.format(track.count)),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -54,197 +54,200 @@ class TrackPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return SafeArea(
|
||||||
headers: const [
|
bottom: false,
|
||||||
TitleBar(
|
child: Scaffold(
|
||||||
automaticallyImplyLeading: true,
|
headers: const [
|
||||||
backgroundColor: Colors.transparent,
|
TitleBar(
|
||||||
surfaceBlur: 0,
|
backgroundColor: Colors.transparent,
|
||||||
)
|
surfaceBlur: 0,
|
||||||
],
|
)
|
||||||
floatingHeader: true,
|
],
|
||||||
child: Stack(
|
floatingHeader: true,
|
||||||
children: [
|
child: Stack(
|
||||||
Positioned.fill(
|
children: [
|
||||||
child: Container(
|
Positioned.fill(
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
image: DecorationImage(
|
decoration: BoxDecoration(
|
||||||
image: UniversalImage.imageProvider(
|
image: DecorationImage(
|
||||||
track.album!.images.asUrlString(
|
image: UniversalImage.imageProvider(
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
track.album!.images.asUrlString(
|
||||||
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
colorFilter: ColorFilter.mode(
|
||||||
|
colorScheme.background.withOpacity(0.5),
|
||||||
|
BlendMode.srcOver,
|
||||||
|
),
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
),
|
),
|
||||||
fit: BoxFit.cover,
|
|
||||||
colorFilter: ColorFilter.mode(
|
|
||||||
colorScheme.background.withOpacity(0.5),
|
|
||||||
BlendMode.srcOver,
|
|
||||||
),
|
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Positioned.fill(
|
||||||
Positioned.fill(
|
child: BackdropFilter(
|
||||||
child: BackdropFilter(
|
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
child: Skeletonizer(
|
||||||
child: Skeletonizer(
|
enabled: trackQuery.isLoading,
|
||||||
enabled: trackQuery.isLoading,
|
child: Container(
|
||||||
child: Container(
|
alignment: Alignment.topCenter,
|
||||||
alignment: Alignment.topCenter,
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
gradient: LinearGradient(
|
||||||
gradient: LinearGradient(
|
colors: [
|
||||||
colors: [
|
colorScheme.background,
|
||||||
colorScheme.background,
|
Colors.transparent,
|
||||||
Colors.transparent,
|
],
|
||||||
],
|
begin: Alignment.topCenter,
|
||||||
begin: Alignment.topCenter,
|
end: Alignment.bottomCenter,
|
||||||
end: Alignment.bottomCenter,
|
stops: const [0.2, 1],
|
||||||
stops: const [0.2, 1],
|
),
|
||||||
),
|
),
|
||||||
),
|
child: SafeArea(
|
||||||
child: SafeArea(
|
child: Wrap(
|
||||||
child: Wrap(
|
spacing: 20,
|
||||||
spacing: 20,
|
runSpacing: 20,
|
||||||
runSpacing: 20,
|
alignment: WrapAlignment.center,
|
||||||
alignment: WrapAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
runAlignment: WrapAlignment.center,
|
||||||
runAlignment: WrapAlignment.center,
|
children: [
|
||||||
children: [
|
ClipRRect(
|
||||||
ClipRRect(
|
borderRadius: BorderRadius.circular(10),
|
||||||
borderRadius: BorderRadius.circular(10),
|
child: UniversalImage(
|
||||||
child: UniversalImage(
|
path: track.album!.images.asUrlString(
|
||||||
path: track.album!.images.asUrlString(
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
),
|
||||||
|
height: 200,
|
||||||
|
width: 200,
|
||||||
),
|
),
|
||||||
height: 200,
|
|
||||||
width: 200,
|
|
||||||
),
|
),
|
||||||
),
|
Padding(
|
||||||
Padding(
|
padding:
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: mediaQuery.smAndDown
|
crossAxisAlignment: mediaQuery.smAndDown
|
||||||
? CrossAxisAlignment.center
|
? CrossAxisAlignment.center
|
||||||
: CrossAxisAlignment.start,
|
: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
track.name!,
|
track.name!,
|
||||||
).large().semiBold(),
|
).large().semiBold(),
|
||||||
const Gap(10),
|
const Gap(10),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
|
||||||
const Icon(SpotubeIcons.album),
|
|
||||||
const Gap(5),
|
|
||||||
Flexible(
|
|
||||||
child: LinkText(
|
|
||||||
track.album!.name!,
|
|
||||||
AlbumRoute(
|
|
||||||
id: track.album!.id!,
|
|
||||||
album: track.album!,
|
|
||||||
),
|
|
||||||
push: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(10),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(SpotubeIcons.artist),
|
|
||||||
const Gap(5),
|
|
||||||
Flexible(
|
|
||||||
child: ArtistLink(
|
|
||||||
artists: track.artists!,
|
|
||||||
hideOverflowArtist: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(10),
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints:
|
|
||||||
const BoxConstraints(maxWidth: 350),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: mediaQuery.smAndDown
|
|
||||||
? MainAxisSize.max
|
|
||||||
: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
|
const Icon(SpotubeIcons.album),
|
||||||
const Gap(5),
|
const Gap(5),
|
||||||
if (!isActive &&
|
Flexible(
|
||||||
!playlist.tracks
|
child: LinkText(
|
||||||
.containsBy(track, (t) => t.id))
|
track.album!.name!,
|
||||||
Button.outline(
|
AlbumRoute(
|
||||||
leading:
|
id: track.album!.id!,
|
||||||
const Icon(SpotubeIcons.queueAdd),
|
album: track.album!,
|
||||||
child: Text(context.l10n.queue),
|
|
||||||
onPressed: () {
|
|
||||||
playlistNotifier.addTrack(track);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Gap(5),
|
|
||||||
if (!isActive &&
|
|
||||||
!playlist.tracks
|
|
||||||
.containsBy(track, (t) => t.id))
|
|
||||||
Tooltip(
|
|
||||||
tooltip: TooltipContainer(
|
|
||||||
child: Text(context.l10n.play_next),
|
|
||||||
),
|
),
|
||||||
child: IconButton.outline(
|
push: true,
|
||||||
icon: const Icon(
|
|
||||||
SpotubeIcons.lightning),
|
|
||||||
onPressed: () {
|
|
||||||
playlistNotifier
|
|
||||||
.addTracksAtFirst([track]);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(5),
|
|
||||||
Tooltip(
|
|
||||||
tooltip: TooltipContainer(
|
|
||||||
child: Text(
|
|
||||||
isActive
|
|
||||||
? context.l10n.pause_playback
|
|
||||||
: context.l10n.play,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: IconButton.primary(
|
|
||||||
shape: ButtonShape.circle,
|
|
||||||
icon: Icon(
|
|
||||||
isActive
|
|
||||||
? SpotubeIcons.pause
|
|
||||||
: SpotubeIcons.play,
|
|
||||||
),
|
|
||||||
onPressed: onPlay,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(5),
|
|
||||||
if (mediaQuery.smAndDown)
|
|
||||||
const Spacer()
|
|
||||||
else
|
|
||||||
const Gap(20),
|
|
||||||
TrackHeartButton(track: track),
|
|
||||||
TrackOptions(
|
|
||||||
track: track,
|
|
||||||
userPlaylist: false,
|
|
||||||
),
|
|
||||||
const Gap(5),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
const Gap(10),
|
||||||
],
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(SpotubeIcons.artist),
|
||||||
|
const Gap(5),
|
||||||
|
Flexible(
|
||||||
|
child: ArtistLink(
|
||||||
|
artists: track.artists!,
|
||||||
|
hideOverflowArtist: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Gap(10),
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints:
|
||||||
|
const BoxConstraints(maxWidth: 350),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: mediaQuery.smAndDown
|
||||||
|
? MainAxisSize.max
|
||||||
|
: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Gap(5),
|
||||||
|
if (!isActive &&
|
||||||
|
!playlist.tracks
|
||||||
|
.containsBy(track, (t) => t.id))
|
||||||
|
Button.outline(
|
||||||
|
leading:
|
||||||
|
const Icon(SpotubeIcons.queueAdd),
|
||||||
|
child: Text(context.l10n.queue),
|
||||||
|
onPressed: () {
|
||||||
|
playlistNotifier.addTrack(track);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(5),
|
||||||
|
if (!isActive &&
|
||||||
|
!playlist.tracks
|
||||||
|
.containsBy(track, (t) => t.id))
|
||||||
|
Tooltip(
|
||||||
|
tooltip: TooltipContainer(
|
||||||
|
child: Text(context.l10n.play_next),
|
||||||
|
),
|
||||||
|
child: IconButton.outline(
|
||||||
|
icon: const Icon(
|
||||||
|
SpotubeIcons.lightning),
|
||||||
|
onPressed: () {
|
||||||
|
playlistNotifier
|
||||||
|
.addTracksAtFirst([track]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(5),
|
||||||
|
Tooltip(
|
||||||
|
tooltip: TooltipContainer(
|
||||||
|
child: Text(
|
||||||
|
isActive
|
||||||
|
? context.l10n.pause_playback
|
||||||
|
: context.l10n.play,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: IconButton.primary(
|
||||||
|
shape: ButtonShape.circle,
|
||||||
|
icon: Icon(
|
||||||
|
isActive
|
||||||
|
? SpotubeIcons.pause
|
||||||
|
: SpotubeIcons.play,
|
||||||
|
),
|
||||||
|
onPressed: onPlay,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(5),
|
||||||
|
if (mediaQuery.smAndDown)
|
||||||
|
const Spacer()
|
||||||
|
else
|
||||||
|
const Gap(20),
|
||||||
|
TrackHeartButton(track: track),
|
||||||
|
TrackOptions(
|
||||||
|
track: track,
|
||||||
|
userPlaylist: false,
|
||||||
|
),
|
||||||
|
const Gap(5),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user