diff --git a/lib/components/Home/Home.dart b/lib/components/Home/Home.dart index dedef0ddf..efb2ddfe7 100644 --- a/lib/components/Home/Home.dart +++ b/lib/components/Home/Home.dart @@ -43,13 +43,14 @@ class Home extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final int titleBarDragMaxWidth = useBreakpointValue( - md: 80, - lg: 256, - sm: 0, - xl: 256, - xxl: 256, + final double titleBarWidth = useBreakpointValue( + sm: 0.0, + md: 80.0, + lg: 256.0, + xl: 256.0, + xxl: 256.0, ); + final extended = ref.watch(sidebarExtendedStateProvider); final _selectedIndex = useState(0); _onSelectedIndexChanged(int index) => _selectedIndex.value = index; @@ -82,7 +83,9 @@ class Home extends HookConsumerWidget { children: [ Container( constraints: BoxConstraints( - maxWidth: titleBarDragMaxWidth.toDouble(), + maxWidth: extended == null + ? titleBarWidth + : (extended ? 256 : 80), ), color: Theme.of(context).navigationRailTheme.backgroundColor, child: MoveWindow(), diff --git a/lib/components/Home/Sidebar.dart b/lib/components/Home/Sidebar.dart index 83c72969a..ed55873cc 100644 --- a/lib/components/Home/Sidebar.dart +++ b/lib/components/Home/Sidebar.dart @@ -1,20 +1,21 @@ import 'package:badges/badges.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:flutter/material.dart'; import 'package:spotube/components/Shared/UniversalImage.dart'; -import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/models/sideBarTiles.dart'; import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Downloader.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; +import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; +final sidebarExtendedStateProvider = StateProvider((ref) => null); + class Sidebar extends HookConsumerWidget { final int selectedIndex; final void Function(int) onSelectedIndexChanged; @@ -46,16 +47,15 @@ class Sidebar extends HookConsumerWidget { final downloadCount = ref.watch( downloaderProvider.select((s) => s.currentlyRunning), ); - - final int titleBarDragMaxWidth = useBreakpointValue( - md: 80, - lg: 256, - sm: 0, - xl: 256, - xxl: 256, - ); + final forceExtended = ref.watch(sidebarExtendedStateProvider); useEffect(() { + if (forceExtended != null) { + if (extended.value != forceExtended) { + extended.value = forceExtended; + } + return; + } if (breakpoints.isMd && extended.value) { extended.value = false; } else if (breakpoints.isMoreThanOrEqualTo(Breakpoints.lg) && @@ -65,7 +65,17 @@ class Sidebar extends HookConsumerWidget { return null; }); - if (breakpoints.isSm) return Container(); + final layoutMode = + ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); + + if (layoutMode == LayoutMode.compact || + (breakpoints.isSm && layoutMode == LayoutMode.adaptive)) { + return Container(); + } + + void toggleExtended() => + ref.read(sidebarExtendedStateProvider.notifier).state = + !(forceExtended ?? extended.value); return SafeArea( child: Material( @@ -76,11 +86,11 @@ class Sidebar extends HookConsumerWidget { if (selectedIndex == 3 && kIsDesktop) SizedBox( height: appWindow.titleBarHeight, - width: titleBarDragMaxWidth.toDouble(), + width: extended.value ? 256 : 80, child: MoveWindow(), ), Padding( - padding: const EdgeInsets.only(left: 15), + padding: const EdgeInsets.only(left: 10), child: (extended.value) ? Row( children: [ @@ -88,11 +98,25 @@ class Sidebar extends HookConsumerWidget { const SizedBox( width: 10, ), - Text("Spotube", - style: Theme.of(context).textTheme.headline4), + Text( + "Spotube", + style: Theme.of(context).textTheme.headline4, + ), + IconButton( + icon: const Icon(Icons.menu_rounded), + onPressed: toggleExtended, + ), ], ) - : _buildSmallLogo(), + : Column( + children: [ + IconButton( + icon: const Icon(Icons.menu_rounded), + onPressed: toggleExtended, + ), + _buildSmallLogo(), + ], + ), ), Expanded( child: NavigationRail( @@ -130,7 +154,7 @@ class Sidebar extends HookConsumerWidget { ), ), SizedBox( - width: titleBarDragMaxWidth.toDouble(), + width: extended.value ? 256 : 80, child: Builder( builder: (context) { final data = meSnapshot.asData?.value; diff --git a/lib/components/Home/SpotubeNavigationBar.dart b/lib/components/Home/SpotubeNavigationBar.dart index e78112578..ab376404d 100644 --- a/lib/components/Home/SpotubeNavigationBar.dart +++ b/lib/components/Home/SpotubeNavigationBar.dart @@ -5,6 +5,7 @@ import 'package:spotube/components/Home/Sidebar.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/models/sideBarTiles.dart'; import 'package:spotube/provider/Downloader.dart'; +import 'package:spotube/provider/UserPreferences.dart'; class SpotubeNavigationBar extends HookConsumerWidget { final int selectedIndex; @@ -22,8 +23,12 @@ class SpotubeNavigationBar extends HookConsumerWidget { downloaderProvider.select((s) => s.currentlyRunning), ); final breakpoint = useBreakpoints(); + final layoutMode = + ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); - if (breakpoint.isMoreThan(Breakpoints.sm)) return const SizedBox(); + if (layoutMode == LayoutMode.extended || + (breakpoint.isMoreThan(Breakpoints.sm) && + layoutMode == LayoutMode.adaptive)) return const SizedBox(); return NavigationBar( destinations: [ ...sidebarTileList.map( diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index 1109c6a8a..5750e1b93 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -8,6 +8,7 @@ import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/models/Logger.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:flutter/material.dart'; +import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class Player extends HookConsumerWidget { @@ -17,6 +18,8 @@ class Player extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { Playback playback = ref.watch(playbackProvider); + final layoutMode = + ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); final breakpoint = useBreakpoints(); @@ -51,7 +54,9 @@ class Player extends HookConsumerWidget { WidgetsBinding.instance.addPostFrameCallback((time) { // clearing the overlay-entry as passing the already available // entry will result in splashing while resizing the window - if (breakpoint.isLessThanOrEqualTo(Breakpoints.md) && + if ((layoutMode == LayoutMode.compact || + (breakpoint.isLessThanOrEqualTo(Breakpoints.md) && + layoutMode == LayoutMode.adaptive)) && entryRef.value == null && playback.track != null) { entryRef.value = OverlayEntry( @@ -75,11 +80,13 @@ class Player extends HookConsumerWidget { return () { disposeOverlay(); }; - }, [breakpoint, playback.track]); + }, [breakpoint, playback.track, layoutMode]); // returning an empty non spacious Container as the overlay will take // place in the global overlay stack aka [_entries] - if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) { + if (layoutMode == LayoutMode.compact || + (breakpoint.isLessThanOrEqualTo(Breakpoints.md) && + layoutMode == LayoutMode.adaptive)) { return Container(); } diff --git a/lib/components/Player/PlayerOverlay.dart b/lib/components/Player/PlayerOverlay.dart index b5dd5cca7..302ea109b 100644 --- a/lib/components/Player/PlayerOverlay.dart +++ b/lib/components/Player/PlayerOverlay.dart @@ -8,6 +8,7 @@ import 'package:spotube/hooks/playback.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/usePaletteColor.dart'; import 'package:spotube/provider/Playback.dart'; +import 'package:spotube/provider/UserPreferences.dart'; class PlayerOverlay extends HookConsumerWidget { final String albumArt; @@ -21,6 +22,9 @@ class PlayerOverlay extends HookConsumerWidget { Widget build(BuildContext context, ref) { final breakpoint = useBreakpoints(); final paletteColor = usePaletteColor(albumArt, ref); + final layoutMode = ref.watch( + userPreferencesProvider.select((s) => s.layoutMode), + ); var isHome = GoRouter.of(context).location == "/"; final isAllowedPage = ["/playlist/", "/album/"].any( @@ -36,8 +40,17 @@ class PlayerOverlay extends HookConsumerWidget { return AnimatedPositioned( duration: const Duration(milliseconds: 2500), right: (breakpoint.isMd && !isAllowedPage ? 10 : 5), - left: (breakpoint.isSm || isAllowedPage ? 5 : 90), - bottom: (breakpoint.isSm && !isAllowedPage ? 63 : 10), + left: (layoutMode == LayoutMode.compact || + (breakpoint.isSm && layoutMode == LayoutMode.adaptive) || + isAllowedPage + ? 5 + : 90), + bottom: (layoutMode == LayoutMode.compact && !isAllowedPage) || + (breakpoint.isSm && + layoutMode == LayoutMode.adaptive && + !isAllowedPage) + ? 63 + : 10, child: GestureDetector( onVerticalDragEnd: (details) { int sensitivity = 8; diff --git a/lib/components/Settings/Settings.dart b/lib/components/Settings/Settings.dart index 9bc358395..2ac7cea5d 100644 --- a/lib/components/Settings/Settings.dart +++ b/lib/components/Settings/Settings.dart @@ -123,6 +123,40 @@ class Settings extends HookConsumerWidget { style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), ), + AdaptiveListTile( + leading: const Icon(Icons.dashboard_rounded), + title: const Text("Layout Mode"), + subtitle: const Text( + "Override responsive layout mode settings", + ), + trailing: (context, update) => DropdownButton( + value: preferences.layoutMode, + items: const [ + DropdownMenuItem( + child: Text( + "Adaptive", + ), + value: LayoutMode.adaptive, + ), + DropdownMenuItem( + child: Text( + "Compact", + ), + value: LayoutMode.compact, + ), + DropdownMenuItem( + child: Text("Extended"), + value: LayoutMode.extended, + ), + ], + onChanged: (value) { + if (value != null) { + preferences.setLayoutMode(value); + update?.call(() {}); + } + }, + ), + ), AdaptiveListTile( leading: const Icon(Icons.dark_mode_outlined), title: const Text("Theme"), diff --git a/lib/models/Logger.dart b/lib/models/Logger.dart index d0a6288cb..029f4c147 100644 --- a/lib/models/Logger.dart +++ b/lib/models/Logger.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:logger/logger.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; +import 'package:spotube/utils/platform.dart'; final _loggerFactory = _SpotubeLogger(); @@ -18,8 +19,11 @@ class _SpotubeLogger extends Logger { @override void log(Level level, message, [error, StackTrace? stackTrace]) { - getApplicationDocumentsDirectory().then((dir) async { - final file = File(path.join(dir.path, ".spotube_logs")); + (kIsAndroid + ? getExternalStorageDirectory() + : getApplicationDocumentsDirectory()) + .then((dir) async { + final file = File(path.join(dir!.path, ".spotube_logs")); if (level == Level.error) { await file.writeAsString("[${DateTime.now()}]\n$message\n$stackTrace", mode: FileMode.writeOnlyAppend); diff --git a/lib/provider/UserPreferences.dart b/lib/provider/UserPreferences.dart index 3f9f00209..3b4f2906a 100644 --- a/lib/provider/UserPreferences.dart +++ b/lib/provider/UserPreferences.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -14,6 +13,12 @@ import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/primitive_utils.dart'; import 'package:path/path.dart' as path; +enum LayoutMode { + compact, + extended, + adaptive, +} + class UserPreferences extends PersistedChangeNotifier { ThemeMode themeMode; String ytSearchFormat; @@ -30,11 +35,14 @@ class UserPreferences extends PersistedChangeNotifier { String downloadLocation; + LayoutMode layoutMode; + UserPreferences({ required this.geniusAccessToken, required this.recommendationMarket, required this.themeMode, required this.ytSearchFormat, + required this.layoutMode, this.saveTrackLyrics = false, this.accentColorScheme = Colors.green, this.backgroundColorScheme = Colors.grey, @@ -126,6 +134,12 @@ class UserPreferences extends PersistedChangeNotifier { updatePersistence(); } + void setLayoutMode(LayoutMode mode) { + layoutMode = mode; + notifyListeners(); + updatePersistence(); + } + Future _getDefaultDownloadDirectory() async { if (kIsAndroid) return "/storage/emulated/0/Download/Spotube"; return getDownloadsDirectory().then((dir) { @@ -158,6 +172,11 @@ class UserPreferences extends PersistedChangeNotifier { skipSponsorSegments = map["skipSponsorSegments"] ?? skipSponsorSegments; downloadLocation = map["downloadLocation"] ?? await _getDefaultDownloadDirectory(); + + layoutMode = LayoutMode.values.firstWhere( + (mode) => mode.name == map["layoutMode"], + orElse: () => kIsDesktop ? LayoutMode.extended : LayoutMode.compact, + ); } @override @@ -175,6 +194,7 @@ class UserPreferences extends PersistedChangeNotifier { "audioQuality": audioQuality.index, "skipSponsorSegments": skipSponsorSegments, "downloadLocation": downloadLocation, + "layoutMode": layoutMode.name, }; } } @@ -185,5 +205,6 @@ final userPreferencesProvider = ChangeNotifierProvider( recommendationMarket: 'US', themeMode: ThemeMode.system, ytSearchFormat: "\$MAIN_ARTIST - \$TITLE \$FEATURED_ARTISTS", + layoutMode: kIsMobile ? LayoutMode.compact : LayoutMode.adaptive, ), );