mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat: desktop mini player support
This commit is contained in:
parent
62ad86e88d
commit
471812d789
@ -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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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');
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
|
88
lib/components/shared/bordered_text.dart
Normal file
88
lib/components/shared/bordered_text.dart
Normal 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,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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());
|
||||
|
177
lib/pages/lyrics/mini_lyrics.dart
Normal file
177
lib/pages/lyrics/mini_lyrics.dart
Normal 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))
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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: [
|
||||
|
@ -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;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user