refactor: alternative track sheet to use shadcn

This commit is contained in:
Kingkor Roy Tirtho 2025-01-03 23:10:26 +06:00
parent f96b5eae97
commit 30e03786bf
3 changed files with 378 additions and 343 deletions

View File

@ -98,7 +98,12 @@ class PlayerView extends HookConsumerWidget {
onPopInvoked: (didPop) async { onPopInvoked: (didPop) async {
await panelController.close(); await panelController.close();
}, },
child: SurfaceCard(
borderWidth: 0,
surfaceOpacity: 0.9,
padding: EdgeInsets.zero,
child: Scaffold( child: Scaffold(
backgroundColor: Colors.transparent,
headers: [ headers: [
SafeArea( SafeArea(
child: TitleBar( child: TitleBar(
@ -319,6 +324,7 @@ class PlayerView extends HookConsumerWidget {
), ),
), ),
), ),
),
); );
} }
} }

View File

@ -5,6 +5,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/modules/player/player_queue.dart';
import 'package:spotube/modules/player/sibling_tracks_sheet.dart'; import 'package:spotube/modules/player/sibling_tracks_sheet.dart';
import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
@ -118,9 +119,28 @@ class PlayerActions extends HookConsumerWidget {
tooltip: TooltipContainer( tooltip: TooltipContainer(
child: Text(context.l10n.alternative_track_sources)), child: Text(context.l10n.alternative_track_sources)),
child: IconButton.ghost( child: IconButton.ghost(
enabled: playlist.activeTrack != null,
icon: const Icon(SpotubeIcons.alternativeRoute), icon: const Icon(SpotubeIcons.alternativeRoute),
onPressed: playlist.activeTrack != null onPressed: () {
? () { final screenSize = MediaQuery.sizeOf(context);
if (screenSize.mdAndUp) {
showPopover(
alignment: Alignment.bottomCenter,
context: context,
builder: (context) {
return SurfaceCard(
padding: EdgeInsets.zero,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 600,
maxWidth: 500,
),
child: SiblingTracksSheet(floating: floatingQueue),
),
);
},
);
} else {
openDrawer( openDrawer(
context: context, context: context,
position: OverlayPosition.bottom, position: OverlayPosition.bottom,
@ -129,12 +149,24 @@ class PlayerActions extends HookConsumerWidget {
barrierColor: Colors.black.withValues(alpha: .2), barrierColor: Colors.black.withValues(alpha: .2),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
transformBackdrop: false, transformBackdrop: false,
surfaceBlur: context.theme.surfaceBlur,
surfaceOpacity: context.theme.surfaceOpacity,
builder: (context) { builder: (context) {
return SiblingTracksSheet(floating: floatingQueue); return Card(
borderWidth: 0,
borderColor: Colors.transparent,
padding: EdgeInsets.zero,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: screenSize.height * .8,
),
child: SiblingTracksSheet(floating: floatingQueue),
),
);
}, },
); );
} }
: null, },
), ),
), ),
if (!kIsWeb && !isLocalTrack) if (!kIsWeb && !isLocalTrack)

View File

@ -1,16 +1,15 @@
import 'dart:ui';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart' show ListTile, Material, MaterialType;
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_extension.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/duration.dart';
import 'package:spotube/hooks/utils/use_debounce.dart'; import 'package:spotube/hooks/utils/use_debounce.dart';
@ -152,13 +151,6 @@ class SiblingTracksSheet extends HookConsumerWidget {
[activeTrack, isFetchingActiveTrack], [activeTrack, isFetchingActiveTrack],
); );
final borderRadius = floating
? BorderRadius.circular(10)
: const BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
);
useEffect(() { useEffect(() {
if (activeTrack is SourcedTrack && activeTrack.siblings.isEmpty) { if (activeTrack is SourcedTrack && activeTrack.siblings.isEmpty) {
activeTrackNotifier.populateSibling(); activeTrackNotifier.populateSibling();
@ -170,9 +162,17 @@ class SiblingTracksSheet extends HookConsumerWidget {
(SourceInfo sourceInfo) { (SourceInfo sourceInfo) {
final icon = sourceInfoToIconMap[sourceInfo.runtimeType]; final icon = sourceInfoToIconMap[sourceInfo.runtimeType];
return ListTile( return ListTile(
hoverColor: theme.colorScheme.primary.withOpacity(.1),
dense: true,
subtitleTextStyle: theme.typography.small.copyWith(
color: theme.colorScheme.mutedForeground,
),
titleTextStyle: theme.typography.normal,
leadingAndTrailingTextStyle: theme.typography.normal,
title: Text(sourceInfo.title), title: Text(sourceInfo.title),
horizontalTitleGap: 0,
leading: Padding( leading: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.only(top: 8.0, right: 8.0),
child: UniversalImage( child: UniversalImage(
path: sourceInfo.thumbnail, path: sourceInfo.thumbnail,
height: 60, height: 60,
@ -192,12 +192,13 @@ class SiblingTracksSheet extends HookConsumerWidget {
enabled: !isFetchingActiveTrack, enabled: !isFetchingActiveTrack,
selected: !isFetchingActiveTrack && selected: !isFetchingActiveTrack &&
sourceInfo.id == (activeTrack as SourcedTrack).sourceInfo.id, sourceInfo.id == (activeTrack as SourcedTrack).sourceInfo.id,
selectedTileColor: theme.popupMenuTheme.color, selectedTileColor: theme.colorScheme.primary.withOpacity(.1),
selectedColor: theme.colorScheme.primary,
onTap: () { onTap: () {
if (!isFetchingActiveTrack && if (!isFetchingActiveTrack &&
sourceInfo.id != (activeTrack as SourcedTrack).sourceInfo.id) { sourceInfo.id != (activeTrack as SourcedTrack).sourceInfo.id) {
activeTrackNotifier.swapSibling(sourceInfo); activeTrackNotifier.swapSibling(sourceInfo);
Navigator.of(context).pop(); closeDrawer(context);
} }
}, },
); );
@ -205,54 +206,42 @@ class SiblingTracksSheet extends HookConsumerWidget {
[activeTrack, siblings], [activeTrack, siblings],
); );
final mediaQuery = MediaQuery.of(context); final scale = context.theme.scaling;
return SafeArea( return SafeArea(
child: ClipRRect( child: Column(
borderRadius: borderRadius, mainAxisSize: MainAxisSize.min,
clipBehavior: Clip.hardEdge, children: [
child: BackdropFilter( Padding(
filter: ImageFilter.blur( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16),
sigmaX: 12.0, child: Row(
sigmaY: 12.0, spacing: 5,
), children: [
child: AnimatedSize( AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Container(
height: isSearching.value && mediaQuery.smAndDown
? mediaQuery.size.height - 50
: mediaQuery.size.height * .6,
decoration: BoxDecoration(
borderRadius: borderRadius,
color:
theme.colorScheme.surfaceContainerHighest.withOpacity(.5),
),
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
centerTitle: true,
title: AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: !isSearching.value child: !isSearching.value
? Text( ? Text(
context.l10n.alternative_track_sources, context.l10n.alternative_track_sources,
style: theme.textTheme.headlineSmall, style: theme.typography.bold,
) )
: TextField( : Flexible(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 320 * scale,
maxHeight: 38 * scale,
),
child: TextField(
autofocus: true, autofocus: true,
controller: searchController, controller: searchController,
decoration: InputDecoration( placeholder: Text(context.l10n.search),
hintText: context.l10n.search, style: theme.typography.bold,
hintStyle: theme.textTheme.headlineSmall,
border: InputBorder.none,
),
style: theme.textTheme.headlineSmall,
), ),
), ),
automaticallyImplyLeading: false, ),
backgroundColor: Colors.transparent, ),
actions: [ const Spacer(),
if (!isSearching.value) if (!isSearching.value)
IconButton( IconButton.outline(
icon: const Icon(SpotubeIcons.search, size: 18), icon: const Icon(SpotubeIcons.search, size: 18),
onPressed: () { onPressed: () {
isSearching.value = true; isSearching.value = true;
@ -260,22 +249,31 @@ class SiblingTracksSheet extends HookConsumerWidget {
) )
else ...[ else ...[
if (preferences.audioSource == AudioSource.piped) if (preferences.audioSource == AudioSource.piped)
PopupMenuButton( IconButton.outline(
icon: const Icon(SpotubeIcons.filter, size: 18), icon: const Icon(SpotubeIcons.filter, size: 18),
onSelected: (SearchMode mode) { onPressed: () {
searchMode.value = mode; showPopover(
}, context: context,
initialValue: searchMode.value, alignment: Alignment.bottomRight,
itemBuilder: (context) => SearchMode.values builder: (context) {
return DropdownMenu(
children: SearchMode.values
.map( .map(
(e) => PopupMenuItem( (e) => MenuButton(
value: e, onPressed: (context) {
searchMode.value = e;
},
enabled: searchMode.value != e,
child: Text(e.label), child: Text(e.label),
), ),
) )
.toList(), .toList(),
);
},
);
},
), ),
IconButton( IconButton.outline(
icon: const Icon(SpotubeIcons.close, size: 18), icon: const Icon(SpotubeIcons.close, size: 18),
onPressed: () { onPressed: () {
isSearching.value = false; isSearching.value = false;
@ -284,16 +282,19 @@ class SiblingTracksSheet extends HookConsumerWidget {
] ]
], ],
), ),
body: Padding( ),
padding: const EdgeInsets.all(8.0), Expanded(
child: AnimatedSwitcher( child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) => transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child), FadeTransition(opacity: animation, child: child),
child: InterScrollbar( child: InterScrollbar(
controller: controller, controller: controller,
child: Material(
type: MaterialType.transparency,
child: switch (isSearching.value) { child: switch (isSearching.value) {
false => ListView.builder( false => ListView.builder(
padding: const EdgeInsets.all(8.0),
controller: controller, controller: controller,
itemCount: siblings.length, itemCount: siblings.length,
itemBuilder: (context, index) => itemBuilder: (context, index) =>
@ -311,14 +312,12 @@ class SiblingTracksSheet extends HookConsumerWidget {
child: CircularProgressIndicator()); child: CircularProgressIndicator());
} }
return InterScrollbar( return ListView.builder(
controller: controller, padding: const EdgeInsets.all(8.0),
child: ListView.builder(
controller: controller, controller: controller,
itemCount: snapshot.data!.length, itemCount: snapshot.data!.length,
itemBuilder: (context, index) => itemBuilder: (context, index) =>
itemBuilder(snapshot.data![index]), itemBuilder(snapshot.data![index]),
),
); );
}, },
), ),
@ -327,9 +326,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
), ),
), ),
), ),
), ],
),
),
), ),
); );
} }