refactor: use Appbar titlebar throughout the app

This commit is contained in:
Kingkor Roy Tirtho 2024-12-21 17:02:13 +06:00
parent fcefce4b1b
commit f80ea32de4
33 changed files with 149 additions and 232 deletions

View File

@ -1,89 +1,56 @@
import 'package:flutter/material.dart' hide AppBar;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' show AppBar;
import 'package:shadcn_flutter/shadcn_flutter.dart'
show AppBar, WidgetExtension;
import 'package:spotube/components/titlebar/titlebar_buttons.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/utils/platform.dart';
import 'package:window_manager/window_manager.dart';
class PageWindowTitleBar extends StatefulHookConsumerWidget
implements PreferredSizeWidget {
final Widget? leading;
class TitleBar extends HookConsumerWidget implements PreferredSizeWidget {
final bool automaticallyImplyLeading;
final List<Widget>? actions;
final List<Widget> trailing;
final List<Widget> leading;
final Widget? child;
final Widget? title;
final Widget? header; // small widget placed on top of title
final Widget? subtitle; // small widget placed below title
final bool
trailingExpanded; // expand the trailing instead of the main content
final AlignmentGeometry alignment;
final Color? backgroundColor;
final Color? foregroundColor;
final IconThemeData? actionsIconTheme;
final bool? centerTitle;
final double? titleSpacing;
final double toolbarOpacity;
final double? leadingWidth;
final TextStyle? toolbarTextStyle;
final TextStyle? titleTextStyle;
final double? titleWidth;
final Widget? title;
final double? leadingGap;
final double? trailingGap;
final EdgeInsetsGeometry? padding;
final double? height;
final bool useSafeArea;
final double? surfaceBlur;
final double? surfaceOpacity;
final bool _sliver;
const PageWindowTitleBar({
const TitleBar({
super.key,
this.actions,
this.automaticallyImplyLeading = true,
this.trailing = const [],
this.leading = const [],
this.title,
this.toolbarOpacity = 1,
this.header,
this.subtitle,
this.child,
this.trailingExpanded = false,
this.alignment = Alignment.center,
this.padding,
this.backgroundColor,
this.actionsIconTheme,
this.automaticallyImplyLeading = false,
this.centerTitle,
this.foregroundColor,
this.leading,
this.leadingWidth,
this.titleSpacing,
this.titleTextStyle,
this.titleWidth,
this.toolbarTextStyle,
}) : _sliver = false,
pinned = false,
floating = false,
snap = false,
stretch = false;
this.leadingGap,
this.trailingGap,
this.height,
this.surfaceBlur,
this.surfaceOpacity,
this.useSafeArea = true,
});
final bool pinned;
final bool floating;
final bool snap;
final bool stretch;
const PageWindowTitleBar.sliver({
super.key,
this.actions,
this.title,
this.backgroundColor,
this.actionsIconTheme,
this.automaticallyImplyLeading = false,
this.centerTitle,
this.foregroundColor,
this.leading,
this.leadingWidth,
this.titleSpacing,
this.titleTextStyle,
this.titleWidth,
this.toolbarTextStyle,
this.pinned = false,
this.floating = false,
this.snap = false,
this.stretch = false,
}) : _sliver = true,
toolbarOpacity = 1;
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
@override
ConsumerState<PageWindowTitleBar> createState() => _PageWindowTitleBarState();
}
class _PageWindowTitleBarState extends ConsumerState<PageWindowTitleBar> {
void onDrag(details) {
void onDrag(WidgetRef ref) {
final systemTitleBar =
ref.read(userPreferencesProvider.select((s) => s.systemTitleBar));
if (kIsDesktop && !systemTitleBar) {
@ -92,86 +59,53 @@ class _PageWindowTitleBarState extends ConsumerState<PageWindowTitleBar> {
}
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
Widget build(BuildContext context, ref) {
final hasLeadingOrCanPop = leading.isNotEmpty || Navigator.canPop(context);
if (widget._sliver) {
return SliverLayoutBuilder(
return SizedBox(
height: height ?? 56,
child: LayoutBuilder(
builder: (context, constraints) {
final hasFullscreen =
mediaQuery.size.width == constraints.crossAxisExtent;
final hasLeadingOrCanPop =
widget.leading != null || Navigator.canPop(context);
return SliverPadding(
padding: EdgeInsets.only(
left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0,
),
sliver: SliverAppBar(
leading: widget.leading,
automaticallyImplyLeading: widget.automaticallyImplyLeading,
actions: [
...?widget.actions,
WindowTitleBarButtons(foregroundColor: widget.foregroundColor),
],
backgroundColor: widget.backgroundColor,
foregroundColor: widget.foregroundColor,
actionsIconTheme: widget.actionsIconTheme,
centerTitle: widget.centerTitle,
titleSpacing: widget.titleSpacing,
leadingWidth: widget.leadingWidth,
toolbarTextStyle: widget.toolbarTextStyle,
titleTextStyle: widget.titleTextStyle,
title: SizedBox(
width: double.infinity, // workaround to force dragging
child: widget.title ?? const Text(""),
),
pinned: widget.pinned,
floating: widget.floating,
snap: widget.snap,
stretch: widget.stretch,
),
);
},
);
}
return LayoutBuilder(builder: (context, constrains) {
final hasFullscreen = mediaQuery.size.width == constrains.maxWidth;
final hasLeadingOrCanPop =
widget.leading != null || Navigator.canPop(context);
MediaQuery.sizeOf(context).width == constraints.maxWidth;
return GestureDetector(
onHorizontalDragStart: onDrag,
onVerticalDragStart: onDrag,
child: Padding(
padding: EdgeInsets.only(
left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0,
),
onHorizontalDragStart: (_) => onDrag(ref),
onVerticalDragStart: (_) => onDrag(ref),
child: AppBar(
leading: [
if (widget.leading != null) widget.leading!,
if (widget.leading == null &&
widget.automaticallyImplyLeading &&
Navigator.canPop(context))
leading: leading.isEmpty &&
automaticallyImplyLeading &&
Navigator.canPop(context)
? [
const BackButton(),
],
]
: leading,
trailing: [
...?widget.actions,
WindowTitleBarButtons(foregroundColor: widget.foregroundColor),
...trailing,
WindowTitleBarButtons(foregroundColor: foregroundColor),
],
backgroundColor: widget.backgroundColor,
title: SizedBox(
width: double.infinity, // workaround to force dragging
child: widget.title ?? const Text(""),
),
alignment: widget.centerTitle == true
? Alignment.center
: Alignment.centerLeft,
leadingGap: widget.leadingWidth,
),
title: title,
header: header,
subtitle: subtitle,
trailingExpanded: trailingExpanded,
alignment: alignment,
padding: padding,
backgroundColor: backgroundColor,
leadingGap: leadingGap,
trailingGap: trailingGap,
height: height,
surfaceBlur: surfaceBlur,
surfaceOpacity: surfaceOpacity,
useSafeArea: useSafeArea,
child: child,
).withPadding(
left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0),
);
},
),
);
});
}
@override
Size get preferredSize => Size.fromHeight(height ?? 56.0);
}

View File

@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotify/spotify.dart' hide Offset;
import 'package:spotube/collections/assets.gen.dart';
@ -344,6 +345,7 @@ class TrackOptions extends HookConsumerWidget {
leading: const Icon(SpotubeIcons.album),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.go_to_album),
Text(

View File

@ -20,14 +20,14 @@ class TrackView extends HookConsumerWidget {
return Scaffold(
appBar: kIsDesktop
? const PageWindowTitleBar(
? const TitleBar(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
leadingWidth: 400,
leading: Align(
leading: [
Align(
alignment: Alignment.centerLeft,
child: BackButton(color: Colors.white),
),
)
],
)
: null,
extendBodyBehindAppBar: true,

View File

@ -138,15 +138,15 @@ class PlayerView extends HookConsumerWidget {
child: ForceDraggableWidget(
child: Padding(
padding: EdgeInsets.only(top: topPadding),
child: PageWindowTitleBar(
child: TitleBar(
backgroundColor: Colors.transparent,
foregroundColor: titleTextColor,
toolbarOpacity: 1,
leading: IconButton(
leading: [
IconButton(
icon: const Icon(SpotubeIcons.angleDown, size: 18),
onPressed: panelController.close,
),
actions: [
)
],
trailing: [
if (currentTrack is YoutubeSourcedTrack)
TextButton.icon(
icon: Assets.logos.songlinkTransparent.image(

View File

@ -30,8 +30,8 @@ class ArtistPage extends HookConsumerWidget {
return SafeArea(
bottom: false,
child: Scaffold(
appBar: const PageWindowTitleBar(
leading: BackButton(),
appBar: const TitleBar(
leading: [BackButton()],
backgroundColor: Colors.transparent,
),
extendBodyBehindAppBar: true,

View File

@ -23,10 +23,9 @@ class ConnectPage extends HookConsumerWidget {
final discoveredDevices = connectClients.asData?.value.services;
return Scaffold(
appBar: PageWindowTitleBar(
appBar: TitleBar(
automaticallyImplyLeading: true,
title: Text(context.l10n.devices),
titleSpacing: 0,
),
body: ListTileTheme(
shape: RoundedRectangleBorder(

View File

@ -88,7 +88,7 @@ class ConnectControlPage extends HookConsumerWidget {
return SafeArea(
child: Scaffold(
appBar: PageWindowTitleBar(
appBar: TitleBar(
title: Text(resolvedService!.name),
automaticallyImplyLeading: true,
),

View File

@ -43,9 +43,9 @@ class GettingStarting extends HookConsumerWidget {
return Theme(
data: themeData,
child: Scaffold(
appBar: PageWindowTitleBar(
appBar: TitleBar(
backgroundColor: Colors.transparent,
actions: [
trailing: [
ListenableBuilder(
listenable: pageController,
builder: (context, _) {

View File

@ -23,11 +23,9 @@ class HomeFeedSectionPage extends HookConsumerWidget {
return Skeletonizer(
enabled: homeFeedSection.isLoading,
child: Scaffold(
appBar: PageWindowTitleBar(
appBar: TitleBar(
title: Text(section.title ?? ""),
centerTitle: false,
automaticallyImplyLeading: true,
titleSpacing: 0,
),
body: CustomScrollView(
slivers: [

View File

@ -40,8 +40,8 @@ class GenrePlaylistsPage extends HookConsumerWidget {
return Scaffold(
appBar: kIsDesktop
? const PageWindowTitleBar(
leading: BackButton(color: Colors.white),
? const TitleBar(
leading: [BackButton(color: Colors.white)],
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
)

View File

@ -25,10 +25,9 @@ class GenrePage extends HookConsumerWidget {
final mediaQuery = MediaQuery.of(context);
return Scaffold(
appBar: PageWindowTitleBar(
appBar: TitleBar(
title: Text(context.l10n.explore_genres),
automaticallyImplyLeading: true,
titleSpacing: 0,
),
body: SafeArea(
top: false,

View File

@ -34,7 +34,7 @@ class HomePage extends HookConsumerWidget {
return SafeArea(
bottom: false,
child: Scaffold(
appBar: kIsMobile || kIsMacOS ? null : const PageWindowTitleBar(),
appBar: kIsMobile || kIsMacOS ? null : const TitleBar(),
body: CustomScrollView(
controller: controller,
slivers: [

View File

@ -27,7 +27,7 @@ class LastFMLoginPage extends HookConsumerWidget {
final isLoading = useState(false);
return Scaffold(
appBar: const PageWindowTitleBar(leading: BackButton()),
appBar: const TitleBar(leading: [BackButton()]),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),

View File

@ -37,8 +37,9 @@ class LibraryPage extends HookConsumerWidget {
bottom: false,
child: Scaffold(
headers: [
PageWindowTitleBar(
leading: TabList(
TitleBar(
leading: [
TabList(
index: index.value,
children: [
for (final child in children)
@ -49,7 +50,8 @@ class LibraryPage extends HookConsumerWidget {
},
),
],
),
)
],
)
],
child: IndexedStack(
@ -60,11 +62,6 @@ class LibraryPage extends HookConsumerWidget {
UserDownloads(),
UserArtists(),
UserAlbums(),
// Text("UserPlaylists()"),
// Text("UserLocalTracks()"),
// Text("UserDownloads()"),
// Text("UserArtists()"),
// Text("UserAlbums()"),
],
),
),

View File

@ -93,9 +93,8 @@ class LocalLibraryPage extends HookConsumerWidget {
return SafeArea(
bottom: false,
child: Scaffold(
appBar: PageWindowTitleBar(
leading: const BackButton(),
centerTitle: true,
appBar: TitleBar(
leading: const [BackButton()],
title: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@ -120,7 +119,7 @@ class LocalLibraryPage extends HookConsumerWidget {
],
),
backgroundColor: Colors.transparent,
actions: [
trailing: [
if (isCache) ...[
IconButton(
iconSize: 16,

View File

@ -231,10 +231,9 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
final controller = useScrollController();
return Scaffold(
appBar: PageWindowTitleBar(
leading: const BackButton(),
appBar: TitleBar(
leading: const [BackButton()],
title: Text(context.l10n.generate_playlist),
centerTitle: true,
),
body: Scrollbar(
controller: controller,

View File

@ -48,7 +48,7 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
(generatedPlaylist.asData?.value.length ?? 0);
return Scaffold(
appBar: const PageWindowTitleBar(leading: BackButton()),
appBar: const TitleBar(leading: [BackButton()]),
body: generatedPlaylist.isLoading
? Center(
child: Column(

View File

@ -146,7 +146,7 @@ class LyricsPage extends HookConsumerWidget {
child: Scaffold(
extendBodyBehindAppBar: true,
appBar: !kIsMacOS
? PageWindowTitleBar(
? TitleBar(
backgroundColor: Colors.transparent,
title: tabbar,
)

View File

@ -24,8 +24,8 @@ class WebViewLogin extends HookConsumerWidget {
}
return Scaffold(
appBar: const PageWindowTitleBar(
leading: BackButton(color: Colors.white),
appBar: const TitleBar(
leading: [BackButton(color: Colors.white)],
backgroundColor: Colors.transparent,
),
extendBodyBehindAppBar: true,

View File

@ -42,11 +42,9 @@ class ProfilePage extends HookConsumerWidget {
return SafeArea(
child: Scaffold(
appBar: PageWindowTitleBar(
appBar: TitleBar(
title: Text(context.l10n.profile),
titleSpacing: 0,
automaticallyImplyLeading: true,
centerTitle: false,
),
body: Skeletonizer(
enabled: me.isLoading,

View File

@ -88,7 +88,7 @@ class SearchPage extends HookConsumerWidget {
bottom: false,
child: Scaffold(
appBar: kIsDesktop && !kIsMacOS
? const PageWindowTitleBar(automaticallyImplyLeading: true)
? const TitleBar(automaticallyImplyLeading: true)
: null,
body: auth.asData?.value == null
? const AnonymousFallback()

View File

@ -29,8 +29,8 @@ class AboutSpotube extends HookConsumerWidget {
const colon = Text(":");
return Scaffold(
appBar: PageWindowTitleBar(
leading: const BackButton(),
appBar: TitleBar(
leading: const [BackButton()],
title: Text(context.l10n.about_spotube),
),
body: SingleChildScrollView(

View File

@ -44,10 +44,9 @@ class BlackListPage extends HookConsumerWidget {
);
return Scaffold(
appBar: PageWindowTitleBar(
appBar: TitleBar(
title: Text(context.l10n.blacklist),
centerTitle: true,
leading: const BackButton(),
leading: const [BackButton()],
),
body: Column(
mainAxisSize: MainAxisSize.min,

View File

@ -21,10 +21,10 @@ class LogsPage extends HookConsumerWidget {
final logsQuery = ref.watch(logsProvider);
return Scaffold(
appBar: PageWindowTitleBar(
appBar: TitleBar(
title: Text(context.l10n.logs),
leading: const BackButton(),
actions: [
leading: const [BackButton()],
trailing: [
IconButton(
icon: const Icon(SpotubeIcons.clipboard),
iconSize: 16,

View File

@ -28,9 +28,8 @@ class SettingsPage extends HookConsumerWidget {
return SafeArea(
bottom: false,
child: Scaffold(
appBar: PageWindowTitleBar(
appBar: TitleBar(
title: Text(context.l10n.settings),
centerTitle: true,
automaticallyImplyLeading: true,
),
body: Scrollbar(

View File

@ -25,9 +25,8 @@ class StatsAlbumsPage extends HookConsumerWidget {
final albumsData = topAlbums.asData?.value.items ?? [];
return Scaffold(
appBar: PageWindowTitleBar(
appBar: TitleBar(
automaticallyImplyLeading: true,
centerTitle: false,
title: Text(context.l10n.albums),
),
body: Skeletonizer(

View File

@ -28,9 +28,8 @@ class StatsArtistsPage extends HookConsumerWidget {
() => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]);
return Scaffold(
appBar: PageWindowTitleBar(
appBar: TitleBar(
automaticallyImplyLeading: true,
centerTitle: false,
title: Text(context.l10n.artists),
),
body: Skeletonizer(

View File

@ -41,9 +41,8 @@ class StatsStreamFeesPage extends HookConsumerWidget {
);
return Scaffold(
appBar: PageWindowTitleBar(
appBar: TitleBar(
automaticallyImplyLeading: true,
centerTitle: false,
title: Text(context.l10n.streaming_fees_hypothetical),
),
body: CustomScrollView(

View File

@ -28,9 +28,8 @@ class StatsMinutesPage extends HookConsumerWidget {
final tracksData = topTracks.asData?.value.items ?? [];
return Scaffold(
appBar: PageWindowTitleBar(
appBar: TitleBar(
title: Text(context.l10n.minutes_listened),
centerTitle: false,
automaticallyImplyLeading: true,
),
body: Skeletonizer(

View File

@ -26,9 +26,8 @@ class StatsPlaylistsPage extends HookConsumerWidget {
final playlistsData = topPlaylists.asData?.value.items ?? [];
return Scaffold(
appBar: PageWindowTitleBar(
appBar: TitleBar(
automaticallyImplyLeading: true,
centerTitle: false,
title: Text(context.l10n.playlists),
),
body: Skeletonizer(

View File

@ -16,7 +16,7 @@ class StatsPage extends HookConsumerWidget {
return SafeArea(
bottom: false,
child: Scaffold(
appBar: kIsMacOS || kIsMobile ? null : const PageWindowTitleBar(),
appBar: kIsMacOS || kIsMobile ? null : const TitleBar(),
body: CustomScrollView(
slivers: [
if (kIsMacOS) const SliverGap(20),

View File

@ -28,9 +28,8 @@ class StatsStreamsPage extends HookConsumerWidget {
final tracksData = topTracks.asData?.value.items ?? [];
return Scaffold(
appBar: PageWindowTitleBar(
appBar: TitleBar(
title: Text(context.l10n.streamed_songs),
centerTitle: false,
automaticallyImplyLeading: true,
),
body: Skeletonizer(

View File

@ -53,7 +53,7 @@ class TrackPage extends HookConsumerWidget {
}
return Scaffold(
appBar: const PageWindowTitleBar(
appBar: const TitleBar(
automaticallyImplyLeading: true,
backgroundColor: Colors.transparent,
),