fix(layout): Fix adaptive UI not working correctly by providing a overriding option

This commit is contained in:
Kingkor Roy Tirtho 2022-09-12 15:02:47 +06:00
parent 192acc169e
commit 8c7adde890
8 changed files with 144 additions and 33 deletions

View File

@ -43,13 +43,14 @@ class Home extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final int titleBarDragMaxWidth = useBreakpointValue( final double titleBarWidth = useBreakpointValue(
md: 80, sm: 0.0,
lg: 256, md: 80.0,
sm: 0, lg: 256.0,
xl: 256, xl: 256.0,
xxl: 256, xxl: 256.0,
); );
final extended = ref.watch(sidebarExtendedStateProvider);
final _selectedIndex = useState(0); final _selectedIndex = useState(0);
_onSelectedIndexChanged(int index) => _selectedIndex.value = index; _onSelectedIndexChanged(int index) => _selectedIndex.value = index;
@ -82,7 +83,9 @@ class Home extends HookConsumerWidget {
children: [ children: [
Container( Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: titleBarDragMaxWidth.toDouble(), maxWidth: extended == null
? titleBarWidth
: (extended ? 256 : 80),
), ),
color: Theme.of(context).navigationRailTheme.backgroundColor, color: Theme.of(context).navigationRailTheme.backgroundColor,
child: MoveWindow(), child: MoveWindow(),

View File

@ -1,20 +1,21 @@
import 'package:badges/badges.dart'; import 'package:badges/badges.dart';
import 'package:bitsdojo_window/bitsdojo_window.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:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotube/components/Shared/UniversalImage.dart'; import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/hooks/useBreakpointValue.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/sideBarTiles.dart'; import 'package:spotube/models/sideBarTiles.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Downloader.dart'; import 'package:spotube/provider/Downloader.dart';
import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
final sidebarExtendedStateProvider = StateProvider<bool?>((ref) => null);
class Sidebar extends HookConsumerWidget { class Sidebar extends HookConsumerWidget {
final int selectedIndex; final int selectedIndex;
final void Function(int) onSelectedIndexChanged; final void Function(int) onSelectedIndexChanged;
@ -46,16 +47,15 @@ class Sidebar extends HookConsumerWidget {
final downloadCount = ref.watch( final downloadCount = ref.watch(
downloaderProvider.select((s) => s.currentlyRunning), downloaderProvider.select((s) => s.currentlyRunning),
); );
final forceExtended = ref.watch(sidebarExtendedStateProvider);
final int titleBarDragMaxWidth = useBreakpointValue(
md: 80,
lg: 256,
sm: 0,
xl: 256,
xxl: 256,
);
useEffect(() { useEffect(() {
if (forceExtended != null) {
if (extended.value != forceExtended) {
extended.value = forceExtended;
}
return;
}
if (breakpoints.isMd && extended.value) { if (breakpoints.isMd && extended.value) {
extended.value = false; extended.value = false;
} else if (breakpoints.isMoreThanOrEqualTo(Breakpoints.lg) && } else if (breakpoints.isMoreThanOrEqualTo(Breakpoints.lg) &&
@ -65,7 +65,17 @@ class Sidebar extends HookConsumerWidget {
return null; 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( return SafeArea(
child: Material( child: Material(
@ -76,11 +86,11 @@ class Sidebar extends HookConsumerWidget {
if (selectedIndex == 3 && kIsDesktop) if (selectedIndex == 3 && kIsDesktop)
SizedBox( SizedBox(
height: appWindow.titleBarHeight, height: appWindow.titleBarHeight,
width: titleBarDragMaxWidth.toDouble(), width: extended.value ? 256 : 80,
child: MoveWindow(), child: MoveWindow(),
), ),
Padding( Padding(
padding: const EdgeInsets.only(left: 15), padding: const EdgeInsets.only(left: 10),
child: (extended.value) child: (extended.value)
? Row( ? Row(
children: [ children: [
@ -88,11 +98,25 @@ class Sidebar extends HookConsumerWidget {
const SizedBox( const SizedBox(
width: 10, width: 10,
), ),
Text("Spotube", Text(
style: Theme.of(context).textTheme.headline4), "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( Expanded(
child: NavigationRail( child: NavigationRail(
@ -130,7 +154,7 @@ class Sidebar extends HookConsumerWidget {
), ),
), ),
SizedBox( SizedBox(
width: titleBarDragMaxWidth.toDouble(), width: extended.value ? 256 : 80,
child: Builder( child: Builder(
builder: (context) { builder: (context) {
final data = meSnapshot.asData?.value; final data = meSnapshot.asData?.value;

View File

@ -5,6 +5,7 @@ import 'package:spotube/components/Home/Sidebar.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/sideBarTiles.dart'; import 'package:spotube/models/sideBarTiles.dart';
import 'package:spotube/provider/Downloader.dart'; import 'package:spotube/provider/Downloader.dart';
import 'package:spotube/provider/UserPreferences.dart';
class SpotubeNavigationBar extends HookConsumerWidget { class SpotubeNavigationBar extends HookConsumerWidget {
final int selectedIndex; final int selectedIndex;
@ -22,8 +23,12 @@ class SpotubeNavigationBar extends HookConsumerWidget {
downloaderProvider.select((s) => s.currentlyRunning), downloaderProvider.select((s) => s.currentlyRunning),
); );
final breakpoint = useBreakpoints(); 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( return NavigationBar(
destinations: [ destinations: [
...sidebarTileList.map( ...sidebarTileList.map(

View File

@ -8,6 +8,7 @@ import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
class Player extends HookConsumerWidget { class Player extends HookConsumerWidget {
@ -17,6 +18,8 @@ class Player extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
final layoutMode =
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
@ -51,7 +54,9 @@ class Player extends HookConsumerWidget {
WidgetsBinding.instance.addPostFrameCallback((time) { WidgetsBinding.instance.addPostFrameCallback((time) {
// clearing the overlay-entry as passing the already available // clearing the overlay-entry as passing the already available
// entry will result in splashing while resizing the window // 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 && entryRef.value == null &&
playback.track != null) { playback.track != null) {
entryRef.value = OverlayEntry( entryRef.value = OverlayEntry(
@ -75,11 +80,13 @@ class Player extends HookConsumerWidget {
return () { return () {
disposeOverlay(); disposeOverlay();
}; };
}, [breakpoint, playback.track]); }, [breakpoint, playback.track, layoutMode]);
// returning an empty non spacious Container as the overlay will take // returning an empty non spacious Container as the overlay will take
// place in the global overlay stack aka [_entries] // 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(); return Container();
} }

View File

@ -8,6 +8,7 @@ import 'package:spotube/hooks/playback.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/hooks/usePaletteColor.dart'; import 'package:spotube/hooks/usePaletteColor.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/UserPreferences.dart';
class PlayerOverlay extends HookConsumerWidget { class PlayerOverlay extends HookConsumerWidget {
final String albumArt; final String albumArt;
@ -21,6 +22,9 @@ class PlayerOverlay extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final paletteColor = usePaletteColor(albumArt, ref); final paletteColor = usePaletteColor(albumArt, ref);
final layoutMode = ref.watch(
userPreferencesProvider.select((s) => s.layoutMode),
);
var isHome = GoRouter.of(context).location == "/"; var isHome = GoRouter.of(context).location == "/";
final isAllowedPage = ["/playlist/", "/album/"].any( final isAllowedPage = ["/playlist/", "/album/"].any(
@ -36,8 +40,17 @@ class PlayerOverlay extends HookConsumerWidget {
return AnimatedPositioned( return AnimatedPositioned(
duration: const Duration(milliseconds: 2500), duration: const Duration(milliseconds: 2500),
right: (breakpoint.isMd && !isAllowedPage ? 10 : 5), right: (breakpoint.isMd && !isAllowedPage ? 10 : 5),
left: (breakpoint.isSm || isAllowedPage ? 5 : 90), left: (layoutMode == LayoutMode.compact ||
bottom: (breakpoint.isSm && !isAllowedPage ? 63 : 10), (breakpoint.isSm && layoutMode == LayoutMode.adaptive) ||
isAllowedPage
? 5
: 90),
bottom: (layoutMode == LayoutMode.compact && !isAllowedPage) ||
(breakpoint.isSm &&
layoutMode == LayoutMode.adaptive &&
!isAllowedPage)
? 63
: 10,
child: GestureDetector( child: GestureDetector(
onVerticalDragEnd: (details) { onVerticalDragEnd: (details) {
int sensitivity = 8; int sensitivity = 8;

View File

@ -123,6 +123,40 @@ class Settings extends HookConsumerWidget {
style: style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 20), 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<LayoutMode>(
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( AdaptiveListTile(
leading: const Icon(Icons.dark_mode_outlined), leading: const Icon(Icons.dark_mode_outlined),
title: const Text("Theme"), title: const Text("Theme"),

View File

@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:spotube/utils/platform.dart';
final _loggerFactory = _SpotubeLogger(); final _loggerFactory = _SpotubeLogger();
@ -18,8 +19,11 @@ class _SpotubeLogger extends Logger {
@override @override
void log(Level level, message, [error, StackTrace? stackTrace]) { void log(Level level, message, [error, StackTrace? stackTrace]) {
getApplicationDocumentsDirectory().then((dir) async { (kIsAndroid
final file = File(path.join(dir.path, ".spotube_logs")); ? getExternalStorageDirectory()
: getApplicationDocumentsDirectory())
.then((dir) async {
final file = File(path.join(dir!.path, ".spotube_logs"));
if (level == Level.error) { if (level == Level.error) {
await file.writeAsString("[${DateTime.now()}]\n$message\n$stackTrace", await file.writeAsString("[${DateTime.now()}]\n$message\n$stackTrace",
mode: FileMode.writeOnlyAppend); mode: FileMode.writeOnlyAppend);

View File

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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:spotube/utils/primitive_utils.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
enum LayoutMode {
compact,
extended,
adaptive,
}
class UserPreferences extends PersistedChangeNotifier { class UserPreferences extends PersistedChangeNotifier {
ThemeMode themeMode; ThemeMode themeMode;
String ytSearchFormat; String ytSearchFormat;
@ -30,11 +35,14 @@ class UserPreferences extends PersistedChangeNotifier {
String downloadLocation; String downloadLocation;
LayoutMode layoutMode;
UserPreferences({ UserPreferences({
required this.geniusAccessToken, required this.geniusAccessToken,
required this.recommendationMarket, required this.recommendationMarket,
required this.themeMode, required this.themeMode,
required this.ytSearchFormat, required this.ytSearchFormat,
required this.layoutMode,
this.saveTrackLyrics = false, this.saveTrackLyrics = false,
this.accentColorScheme = Colors.green, this.accentColorScheme = Colors.green,
this.backgroundColorScheme = Colors.grey, this.backgroundColorScheme = Colors.grey,
@ -126,6 +134,12 @@ class UserPreferences extends PersistedChangeNotifier {
updatePersistence(); updatePersistence();
} }
void setLayoutMode(LayoutMode mode) {
layoutMode = mode;
notifyListeners();
updatePersistence();
}
Future<String> _getDefaultDownloadDirectory() async { Future<String> _getDefaultDownloadDirectory() async {
if (kIsAndroid) return "/storage/emulated/0/Download/Spotube"; if (kIsAndroid) return "/storage/emulated/0/Download/Spotube";
return getDownloadsDirectory().then((dir) { return getDownloadsDirectory().then((dir) {
@ -158,6 +172,11 @@ class UserPreferences extends PersistedChangeNotifier {
skipSponsorSegments = map["skipSponsorSegments"] ?? skipSponsorSegments; skipSponsorSegments = map["skipSponsorSegments"] ?? skipSponsorSegments;
downloadLocation = downloadLocation =
map["downloadLocation"] ?? await _getDefaultDownloadDirectory(); map["downloadLocation"] ?? await _getDefaultDownloadDirectory();
layoutMode = LayoutMode.values.firstWhere(
(mode) => mode.name == map["layoutMode"],
orElse: () => kIsDesktop ? LayoutMode.extended : LayoutMode.compact,
);
} }
@override @override
@ -175,6 +194,7 @@ class UserPreferences extends PersistedChangeNotifier {
"audioQuality": audioQuality.index, "audioQuality": audioQuality.index,
"skipSponsorSegments": skipSponsorSegments, "skipSponsorSegments": skipSponsorSegments,
"downloadLocation": downloadLocation, "downloadLocation": downloadLocation,
"layoutMode": layoutMode.name,
}; };
} }
} }
@ -185,5 +205,6 @@ final userPreferencesProvider = ChangeNotifierProvider(
recommendationMarket: 'US', recommendationMarket: 'US',
themeMode: ThemeMode.system, themeMode: ThemeMode.system,
ytSearchFormat: "\$MAIN_ARTIST - \$TITLE \$FEATURED_ARTISTS", ytSearchFormat: "\$MAIN_ARTIST - \$TITLE \$FEATURED_ARTISTS",
layoutMode: kIsMobile ? LayoutMode.compact : LayoutMode.adaptive,
), ),
); );