feat: desktop mini player support

This commit is contained in:
Kingkor Roy Tirtho 2023-04-15 12:29:07 +06:00
parent 62ad86e88d
commit 471812d789
10 changed files with 392 additions and 91 deletions

View File

@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:spotify/spotify.dart' hide Search;
import 'package:spotube/pages/home/home.dart';
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
import 'package:spotube/pages/search/search.dart';
import 'package:spotube/pages/settings/blacklist.dart';
import 'package:spotube/pages/settings/about.dart';
@ -31,42 +32,51 @@ final router = GoRouter(
routes: [
GoRoute(
path: "/",
pageBuilder: (context, state) => SpotubePage(child: const HomePage()),
pageBuilder: (context, state) => const SpotubePage(child: HomePage()),
),
GoRoute(
path: "/search",
name: "Search",
pageBuilder: (context, state) =>
SpotubePage(child: const SearchPage()),
const SpotubePage(child: SearchPage()),
),
GoRoute(
path: "/library",
name: "Library",
pageBuilder: (context, state) =>
SpotubePage(child: const LibraryPage()),
const SpotubePage(child: LibraryPage()),
),
GoRoute(
path: "/lyrics",
name: "Lyrics",
pageBuilder: (context, state) =>
SpotubePage(child: const LyricsPage()),
const SpotubePage(child: LyricsPage()),
routes: [
GoRoute(
path: "mini",
parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) => const SpotubePage(
child: MiniLyricsPage(),
),
),
],
),
GoRoute(
path: "/settings",
pageBuilder: (context, state) => SpotubePage(
child: const SettingsPage(),
pageBuilder: (context, state) => const SpotubePage(
child: SettingsPage(),
),
routes: [
GoRoute(
path: "blacklist",
pageBuilder: (context, state) => SpotubePage(
child: const BlackListPage(),
pageBuilder: (context, state) => const SpotubePage(
child: BlackListPage(),
),
),
GoRoute(
path: "about",
pageBuilder: (context, state) => SpotubePage(
child: const AboutSpotube(),
pageBuilder: (context, state) => const SpotubePage(
child: AboutSpotube(),
),
),
],
@ -106,16 +116,16 @@ final router = GoRouter(
GoRoute(
path: "/login-tutorial",
parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) => SpotubePage(
child: const LoginTutorial(),
pageBuilder: (context, state) => const SpotubePage(
child: LoginTutorial(),
),
),
GoRoute(
path: "/player",
parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) {
return SpotubePage(
child: const PlayerView(),
return const SpotubePage(
child: PlayerView(),
);
},
),

View File

@ -68,4 +68,8 @@ abstract class SpotubeIcons {
static const zoomIn = FeatherIcons.zoomIn;
static const zoomOut = FeatherIcons.zoomOut;
static const tray = FeatherIcons.chevronDown;
static const miniPlayer = Icons.picture_in_picture_rounded;
static const maximize = FeatherIcons.maximize2;
static const pinOn = Icons.push_pin_rounded;
static const pinOff = Icons.push_pin_outlined;
}

View File

@ -30,7 +30,6 @@ class PlayerActions extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final playlist = ref.watch(PlaylistQueueNotifier.provider);
final playlistNotifier = ref.watch(PlaylistQueueNotifier.provider.notifier);
final isLocalTrack = playlist?.activeTrack is LocalTrack;
final downloader = ref.watch(downloaderProvider);
final isInQueue = downloader.inQueue

View File

@ -13,9 +13,11 @@ import 'package:spotube/utils/primitive_utils.dart';
class PlayerControls extends HookConsumerWidget {
final PaletteGenerator? palette;
final bool compact;
PlayerControls({
this.palette,
this.compact = false,
Key? key,
}) : super(key: key);
@ -78,8 +80,8 @@ class PlayerControls extends HookConsumerWidget {
backgroundColor: accentColor?.color ?? theme.colorScheme.primary,
foregroundColor:
accentColor?.titleTextColor ?? theme.colorScheme.onPrimary,
padding: const EdgeInsets.all(12),
iconSize: 24,
padding: EdgeInsets.all(compact ? 10 : 12),
iconSize: compact ? 18 : 24,
);
return GestureDetector(
@ -97,6 +99,7 @@ class PlayerControls extends HookConsumerWidget {
constraints: const BoxConstraints(maxWidth: 600),
child: Column(
children: [
if (!compact)
HookBuilder(
builder: (context) {
final progressObj = useProgress(ref);

View File

@ -2,9 +2,11 @@ import 'dart:ui';
import 'package:flutter/gestures.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/player/player_actions.dart';
import 'package:spotube/components/player/player_overlay.dart';
import 'package:spotube/components/player/player_track_details.dart';
@ -123,7 +125,17 @@ class BottomPlayer extends HookConsumerWidget {
);
}),
),
PlayerActions()
PlayerActions(
extraActions: [
IconButton(
tooltip: 'Mini Player',
icon: const Icon(SpotubeIcons.miniPlayer),
onPressed: () {
GoRouter.of(context).push('/lyrics/mini');
},
),
],
)
],
),
)

View File

@ -0,0 +1,88 @@
library bordered_text;
import 'package:flutter/widgets.dart';
/// Adds stroke to text widget
/// We can apply a very thin and subtle stroke to a [Text]
/// ```dart
/// BorderedText(
/// strokeWidth: 1.0,
/// text: Text(
/// 'Bordered Text',
/// style: TextStyle(
/// decoration: TextDecoration.none,
/// decorationStyle: TextDecorationStyle.wavy,
/// decorationColor: Colors.red,
/// ),
/// ),
/// )
/// ```
class BorderedText extends StatelessWidget {
const BorderedText({
super.key,
required this.child,
this.strokeCap = StrokeCap.round,
this.strokeJoin = StrokeJoin.round,
this.strokeWidth = 6.0,
this.strokeColor = const Color.fromRGBO(53, 0, 71, 1),
});
/// the stroke cap style
final StrokeCap strokeCap;
/// the stroke joint style
final StrokeJoin strokeJoin;
/// the stroke width
final double strokeWidth;
/// the stroke color
final Color strokeColor;
/// the [Text] widget to apply stroke on
final Text child;
@override
Widget build(BuildContext context) {
TextStyle style;
if (child.style != null) {
style = child.style!.copyWith(
foreground: Paint()
..style = PaintingStyle.stroke
..strokeCap = strokeCap
..strokeJoin = strokeJoin
..strokeWidth = strokeWidth
..color = strokeColor,
color: null,
);
} else {
style = TextStyle(
foreground: Paint()
..style = PaintingStyle.stroke
..strokeCap = strokeCap
..strokeJoin = strokeJoin
..strokeWidth = strokeWidth
..color = strokeColor,
);
}
return Stack(
alignment: Alignment.center,
textDirection: child.textDirection,
children: <Widget>[
Text(
child.data!,
style: style,
maxLines: child.maxLines,
overflow: child.overflow,
semanticsLabel: child.semanticsLabel,
softWrap: child.softWrap,
strutStyle: child.strutStyle,
textAlign: child.textAlign,
textDirection: child.textDirection,
textScaleFactor: child.textScaleFactor,
),
child,
],
);
}
}

View File

@ -26,8 +26,9 @@ import 'package:spotube/services/pocketbase.dart';
import 'package:spotube/services/youtube.dart';
import 'package:spotube/themes/theme.dart';
import 'package:system_theme/system_theme.dart';
import 'package:path_provider/path_provider.dart';
import 'hooks/use_init_sys_tray.dart';
import 'package:spotube/hooks/use_init_sys_tray.dart';
Future<void> main(List<String> rawArgs) async {
final parser = ArgParser();
@ -78,7 +79,10 @@ Future<void> main(List<String> rawArgs) async {
await SystemTheme.accentColor.load();
MetadataGod.initialize();
await QueryClient.initialize(cachePrefix: "oss.krtirtho.spotube");
await QueryClient.initialize(
cachePrefix: "oss.krtirtho.spotube",
cacheDir: (await getApplicationSupportDirectory()).path,
);
Hive.registerAdapter(CacheTrackAdapter());
Hive.registerAdapter(CacheTrackEngagementAdapter());
Hive.registerAdapter(CacheTrackSkipSegmentAdapter());

View File

@ -0,0 +1,177 @@
import 'package:flutter/material.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/player/player_controls.dart';
import 'package:spotube/components/player/player_queue.dart';
import 'package:spotube/components/root/sidebar.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/hooks/use_force_update.dart';
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/playlist_queue_provider.dart';
class MiniLyricsPage extends HookConsumerWidget {
const MiniLyricsPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final update = useForceUpdate();
final prevSize = useRef<Size?>(null);
final playlistQueue = ref.watch(PlaylistQueueNotifier.provider);
useEffect(() {
WidgetsBinding.instance.addPostFrameCallback((_) async {
prevSize.value = await DesktopTools.window.getSize();
await DesktopTools.window.setMinimumSize(const Size(300, 300));
await DesktopTools.window.setAlwaysOnTop(true);
await DesktopTools.window.setSize(const Size(400, 500));
});
return null;
}, []);
final auth = ref.watch(AuthenticationNotifier.provider);
if (auth == null) {
return const Scaffold(
appBar: PageWindowTitleBar(),
body: AnonymousFallback(),
);
}
return DefaultTabController(
length: 2,
child: Scaffold(
backgroundColor: theme.colorScheme.surface.withOpacity(0.4),
appBar: PreferredSize(
preferredSize: const Size.fromHeight(60),
child: DragToMoveArea(
child: Row(
children: [
const SizedBox(width: 10),
SizedBox(
height: 30,
width: 30,
child: Sidebar.brandLogo(),
),
const Spacer(),
const SizedBox(
height: 30,
child: TabBar(
tabs: [Tab(text: 'Synced'), Tab(text: 'Plain')],
isScrollable: true,
),
),
const Spacer(),
FutureBuilder(
future: DesktopTools.window.isAlwaysOnTop(),
builder: (context, snapshot) {
return IconButton(
tooltip: 'Always on top',
icon: Icon(
snapshot.data == true
? SpotubeIcons.pinOn
: SpotubeIcons.pinOff,
),
style: ButtonStyle(
foregroundColor: snapshot.data == true
? MaterialStateProperty.all(
theme.colorScheme.primary)
: null,
),
onPressed: snapshot.data == null
? null
: () async {
await DesktopTools.window.setAlwaysOnTop(
snapshot.data == true ? false : true,
);
update();
},
);
}),
IconButton(
tooltip: 'Exit Mini Player',
icon: const Icon(SpotubeIcons.maximize),
onPressed: () async {
try {
await DesktopTools.window
.setMinimumSize(const Size(300, 700));
await DesktopTools.window.setAlwaysOnTop(false);
await DesktopTools.window.setSize(prevSize.value!);
await DesktopTools.window.setAlignment(Alignment.center);
} finally {
if (context.mounted) GoRouter.of(context).go('/');
}
},
),
],
),
),
),
body: Column(
children: [
if (playlistQueue != null)
Text(
playlistQueue.activeTrack.name!,
style: theme.textTheme.titleMedium,
),
Expanded(
child: TabBarView(
children: [
SyncedLyrics(
palette: PaletteColor(theme.colorScheme.background, 0),
isModal: true,
defaultTextZoom: 65,
),
PlainLyrics(
palette: PaletteColor(theme.colorScheme.background, 0),
isModal: true,
defaultTextZoom: 65,
),
],
),
),
Row(
children: [
IconButton(
icon: const Icon(SpotubeIcons.queue),
tooltip: 'Queue',
onPressed: playlistQueue != null
? () {
showModalBottomSheet(
context: context,
isDismissible: true,
enableDrag: true,
isScrollControlled: true,
backgroundColor: Colors.black12,
barrierColor: Colors.black12,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
constraints: BoxConstraints(
maxHeight:
MediaQuery.of(context).size.height * .7,
),
builder: (context) {
return const PlayerQueue(floating: true);
},
);
}
: null,
),
Flexible(child: PlayerControls(compact: true))
],
)
],
),
),
);
}
}

View File

@ -15,9 +15,11 @@ import 'package:spotube/utils/type_conversion_utils.dart';
class PlainLyrics extends HookConsumerWidget {
final PaletteColor palette;
final bool? isModal;
final int defaultTextZoom;
const PlainLyrics({
required this.palette,
this.isModal,
this.defaultTextZoom = 100,
Key? key,
}) : super(key: key);
@ -31,7 +33,7 @@ class PlainLyrics extends HookConsumerWidget {
final breakpoint = useBreakpoints();
final textTheme = Theme.of(context).textTheme;
final textZoomLevel = useState<int>(100);
final textZoomLevel = useState<int>(defaultTextZoom);
return Stack(
children: [

View File

@ -20,10 +20,12 @@ final _delay = StateProvider<int>((ref) => 0);
class SyncedLyrics extends HookConsumerWidget {
final PaletteColor palette;
final bool? isModal;
final int defaultTextZoom;
const SyncedLyrics({
required this.palette,
this.isModal,
this.defaultTextZoom = 100,
Key? key,
}) : super(key: key);
@ -51,7 +53,7 @@ class SyncedLyrics extends HookConsumerWidget {
[lyricValue],
);
final currentTime = useSyncedLyrics(ref, lyricsMap, delay);
final textZoomLevel = useState<int>(100);
final textZoomLevel = useState<int>(defaultTextZoom);
final textTheme = Theme.of(context).textTheme;