diff --git a/lib/features/player/view/audiobook_player.dart b/lib/features/player/view/audiobook_player.dart index a901ef7..a849b83 100644 --- a/lib/features/player/view/audiobook_player.dart +++ b/lib/features/player/view/audiobook_player.dart @@ -235,3 +235,57 @@ class AudiobookChapterProgressBar extends HookConsumerWidget { // ! TODO remove onTap void onTap() {} + +class ProportionalAdjuster { + final double minValue; + final double maxValue; + + ProportionalAdjuster({this.minValue = 0.0, this.maxValue = 1.0}); + + double adjust(double value, double ratio, {AdjustMethod method = AdjustMethod.linear}) { + switch (method) { + case AdjustMethod.linear: + return _linearAdjust(value, ratio); + case AdjustMethod.exponential: + return _exponentialAdjust(value, ratio); + case AdjustMethod.smooth: + return _smoothAdjust(value, ratio); + } + } + + double _linearAdjust(double value, double ratio) { + if (value <= minValue) return minValue; + if (value >= maxValue) return maxValue; + + double newValue; + if (ratio > 1) { + newValue = value + (maxValue - value) * (ratio - 1); + } else { + newValue = value * ratio; + } + + return newValue.clamp(minValue, maxValue); + } + + double _exponentialAdjust(double value, double ratio) { + if (value <= minValue) return minValue; + if (value >= maxValue) return maxValue; + + double newValue; + if (ratio > 1) { + newValue = maxValue - (maxValue - value) / ratio; + } else { + newValue = value * ratio; + } + + return newValue.clamp(minValue, maxValue); + } + + double _smoothAdjust(double value, double progress) { + progress = progress.clamp(0.0, 1.0); + double target = value > (minValue + maxValue) / 2 ? maxValue : minValue; + return value + (target - value) * progress; + } +} + +enum AdjustMethod { linear, exponential, smooth } diff --git a/lib/features/player/view/player_when_expanded.dart b/lib/features/player/view/player_when_expanded.dart index 1bb866d..31570c5 100644 --- a/lib/features/player/view/player_when_expanded.dart +++ b/lib/features/player/view/player_when_expanded.dart @@ -43,9 +43,16 @@ class PlayerWhenExpanded extends HookConsumerWidget { earlyEnd, ) .clamp(0.0, 1.0); - final currentChapter = ref.watch(currentPlayingChapterProvider); final currentBookMetadata = ref.watch(currentBookMetadataProvider); + + final adjuster = ProportionalAdjuster(); + final chapterPercentage = adjuster.adjust(earlyPercentage, 1.1); + final authorPercentage = adjuster.adjust(earlyPercentage, 1.2); + final progressPercentage = adjuster.adjust(earlyPercentage, 1.4); + final playPercentage = adjuster.adjust(earlyPercentage, 1.6); + final settingsPercentage = adjuster.adjust(earlyPercentage, 1.8); + return Column( children: [ // sized box for system status bar; not needed as not full screen @@ -128,9 +135,9 @@ class PlayerWhenExpanded extends HookConsumerWidget { ), // the chapter title - if (earlyPercentage > 0.4) + if (chapterPercentage >= 1) Opacity( - opacity: earlyPercentage, + opacity: chapterPercentage, child: Padding( padding: EdgeInsets.only( top: AppElementSizes.paddingRegular * 4 * earlyPercentage, @@ -152,9 +159,9 @@ class PlayerWhenExpanded extends HookConsumerWidget { ), // the book name and author - if (earlyPercentage > 0.5) + if (authorPercentage >= 1) Opacity( - opacity: earlyPercentage, + opacity: authorPercentage, child: Padding( padding: EdgeInsets.only( bottom: AppElementSizes.paddingRegular * earlyPercentage, @@ -177,17 +184,17 @@ class PlayerWhenExpanded extends HookConsumerWidget { // ), ), ), + if (authorPercentage >= 1) const Spacer(), - if (earlyPercentage > 0.5) const Spacer(), // the progress bar - if (earlyPercentage > 0.6) + if (progressPercentage >= 1) Opacity( - opacity: earlyPercentage, + opacity: progressPercentage, child: SizedBox( width: imageSize, child: Padding( padding: EdgeInsets.only( - top: AppElementSizes.paddingRegular * earlyPercentage, + // top: AppElementSizes.paddingRegular * earlyPercentage, left: AppElementSizes.paddingRegular * earlyPercentage, right: AppElementSizes.paddingRegular * earlyPercentage, ), @@ -195,12 +202,12 @@ class PlayerWhenExpanded extends HookConsumerWidget { ), ), ), - if (earlyPercentage > 0.6) const Spacer(), + if (progressPercentage >= 1) const Spacer(), // the chapter skip buttons, seek 30 seconds back and forward, and play/pause button - if (earlyPercentage > 0.8) + if (playPercentage >= 1) Opacity( - opacity: earlyPercentage, + opacity: playPercentage, child: SizedBox( width: imageSize, child: Row( @@ -221,23 +228,26 @@ class PlayerWhenExpanded extends HookConsumerWidget { ), ), ), - if (earlyPercentage > 0.8) const Spacer(), + if (playPercentage >= 1) const Spacer(), // speed control, sleep timer, chapter list, and settings - if (earlyPercentage > 0.9) + if (settingsPercentage >= 1) Opacity( - opacity: earlyPercentage, - child: Padding( - padding: EdgeInsets.only( - bottom: AppElementSizes.paddingRegular * 4 * earlyPercentage, - ), + opacity: settingsPercentage, + child: SizedBox( + // padding: EdgeInsets.only( + // bottom: AppElementSizes.paddingRegular * 4 * earlyPercentage, + // ), + width: imageSize, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // speed control const PlayerSpeedAdjustButton(), + const Spacer(), // sleep timer const SleepTimerButton(), + const Spacer(), // chapter list const ChapterSelectionButton(), // settings diff --git a/lib/features/player/view/player_when_minimized.dart b/lib/features/player/view/player_when_minimized.dart index c9ca295..541de63 100644 --- a/lib/features/player/view/player_when_minimized.dart +++ b/lib/features/player/view/player_when_minimized.dart @@ -37,8 +37,12 @@ class PlayerWhenMinimized extends HookConsumerWidget { final bookMetaExpanded = ref.watch(currentBookMetadataProvider); var barHeight = vanishingPercentage * 3; + + final adjuster = ProportionalAdjuster(); + final rewindPercentage = adjuster.adjust(vanishingPercentage, 1.5); + final playPercentage = adjuster.adjust(vanishingPercentage, 1.8); return Stack( - alignment: Alignment.bottomCenter, + alignment: Alignment.topCenter, children: [ Row( children: [ @@ -100,11 +104,10 @@ class PlayerWhenMinimized extends HookConsumerWidget { // controller.animateToHeight(state: PanelState.MAX); // }, // ), - // rewind button - if (vanishingPercentage > 0.6) + if (rewindPercentage >= 1) Opacity( - opacity: vanishingPercentage, + opacity: rewindPercentage, child: Padding( padding: const EdgeInsets.only(left: 8), child: IconButton( @@ -117,9 +120,9 @@ class PlayerWhenMinimized extends HookConsumerWidget { ), ), // play/pause button - if (vanishingPercentage > 0.8) + if (playPercentage >= 1) Opacity( - opacity: vanishingPercentage, + opacity: playPercentage, child: Padding( padding: const EdgeInsets.only(right: 8), child: AudiobookPlayerPlayPauseButton( @@ -129,6 +132,7 @@ class PlayerWhenMinimized extends HookConsumerWidget { ), ], ), + const Spacer(), SizedBox( height: barHeight, child: LinearProgressIndicator( diff --git a/lib/router/scaffold_with_nav_bar.dart b/lib/router/scaffold_with_nav_bar.dart index 27c3355..510c5ac 100644 --- a/lib/router/scaffold_with_nav_bar.dart +++ b/lib/router/scaffold_with_nav_bar.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -33,16 +35,8 @@ class ScaffoldWithNavBar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - // playerExpandProgress is used to animate bottom navigation bar to opacity 0 and slide down when player is expanded - // final playerProgress = - // useValueListenable(ref.watch(playerExpandProgressNotifierProvider)); final playerProgress = ref.watch(playerHeightProvider); - final playerMaxHeight = MediaQuery.of(context).size.height; - var percentExpandedMiniPlayer = (playerProgress - playerMinHeight) / - (playerMaxHeight - playerMinHeight); - // Clamp the value between 0 and 1 - percentExpandedMiniPlayer = percentExpandedMiniPlayer.clamp(0.0, 1.0); - + final isMobile = Platform.isAndroid || Platform.isIOS; onBackButtonPressed() async { final isPlayerExpanded = playerProgress != playerMinHeight; @@ -96,61 +90,129 @@ class ScaffoldWithNavBar extends HookConsumerWidget { return BackButtonListener( onBackButtonPressed: onBackButtonPressed, child: Scaffold( - body: Stack( - children: [ - navigationShell, - const AudiobookPlayer(), - ], - ), - bottomNavigationBar: Opacity( - // Opacity is interpolated from 1 to 0 when player is expanded - opacity: 1 - percentExpandedMiniPlayer, - child: NavigationBar( - elevation: 0.0, - height: bottomBarHeight * (1 - percentExpandedMiniPlayer), + body: isMobile + ? Stack( + children: [ + navigationShell, + const AudiobookPlayer(), + ], + ) + : buildNavLeft(context, ref), + bottomNavigationBar: isMobile ? buildNavBottom(context, ref) : null, + ), + ); + } - // TODO: get destinations from the navigationShell - // Here, the items of BottomNavigationBar are hard coded. In a real - // world scenario, the items would most likely be generated from the - // branches of the shell route, which can be fetched using - // `navigationShell.route.branches`. + Widget buildNavLeft(BuildContext context, WidgetRef ref) { + return Row( + children: [ + SafeArea( + child: NavigationRail( + minWidth: 60, + minExtendedWidth: 120, + extended: MediaQuery.of(context).size.width > 640, + // extended: false, destinations: _navigationItems.map((item) { final isDestinationLibrary = item.name == 'Library'; - var currentLibrary = - ref.watch(currentLibraryProvider).valueOrNull; + var currentLibrary = ref.watch(currentLibraryProvider).valueOrNull; final libraryIcon = AbsIcons.getIconByName( currentLibrary?.icon, ); - final destinationWidget = NavigationDestination( + final destinationWidget = NavigationRailDestination( icon: Icon( isDestinationLibrary ? libraryIcon ?? item.icon : item.icon, ), selectedIcon: Icon( - isDestinationLibrary - ? libraryIcon ?? item.activeIcon - : item.activeIcon, + isDestinationLibrary ? libraryIcon ?? item.activeIcon : item.activeIcon, ), - label: isDestinationLibrary - ? currentLibrary?.name ?? item.name - : item.name, - tooltip: item.tooltip, + label: Text(isDestinationLibrary ? currentLibrary?.name ?? item.name : item.name), + // tooltip: item.tooltip, ); - if (isDestinationLibrary) { - return GestureDetector( - onSecondaryTap: () => showLibrarySwitcher(context, ref), - onDoubleTap: () => showLibrarySwitcher(context, ref), - child: - destinationWidget, // Wrap the actual NavigationDestination - ); - } else { - // Return the unwrapped destination for other items - return destinationWidget; - } + // if (isDestinationLibrary) { + // return GestureDetector( + // onSecondaryTap: () => showLibrarySwitcher(context, ref), + // onDoubleTap: () => showLibrarySwitcher(context, ref), + // child: + // destinationWidget, // Wrap the actual NavigationDestination + // ); + // } else { + // // Return the unwrapped destination for other items + // return destinationWidget; + // } + return destinationWidget; + // return NavigationRailDestination(icon: Icon(nav.icon), label: Text(nav.name)); }).toList(), selectedIndex: navigationShell.currentIndex, - onDestinationSelected: (int index) => _onTap(context, index, ref), + onDestinationSelected: (int index) { + print(index); + _onTap(context, index, ref); + }, ), ), + VerticalDivider(width: 0.5, thickness: 0.5), + Expanded( + child: Stack( + children: [ + navigationShell, + const AudiobookPlayer(), + ], + ), + ), + ], + ); + } + + Widget buildNavBottom(BuildContext context, WidgetRef ref) { + // playerExpandProgress is used to animate bottom navigation bar to opacity 0 and slide down when player is expanded + // final playerProgress = + // useValueListenable(ref.watch(playerExpandProgressNotifierProvider)); + final playerProgress = ref.watch(playerHeightProvider); + final playerMaxHeight = MediaQuery.of(context).size.height; + var percentExpandedMiniPlayer = + (playerProgress - playerMinHeight) / (playerMaxHeight - playerMinHeight); + // Clamp the value between 0 and 1 + percentExpandedMiniPlayer = percentExpandedMiniPlayer.clamp(0.0, 1.0); + return Opacity( + // Opacity is interpolated from 1 to 0 when player is expanded + opacity: 1 - percentExpandedMiniPlayer, + child: NavigationBar( + elevation: 0.0, + height: bottomBarHeight * (1 - percentExpandedMiniPlayer), + + // TODO: get destinations from the navigationShell + // Here, the items of BottomNavigationBar are hard coded. In a real + // world scenario, the items would most likely be generated from the + // branches of the shell route, which can be fetched using + // `navigationShell.route.branches`. + destinations: _navigationItems.map((item) { + final isDestinationLibrary = item.name == 'Library'; + var currentLibrary = ref.watch(currentLibraryProvider).valueOrNull; + final libraryIcon = AbsIcons.getIconByName( + currentLibrary?.icon, + ); + final destinationWidget = NavigationDestination( + icon: Icon( + isDestinationLibrary ? libraryIcon ?? item.icon : item.icon, + ), + selectedIcon: Icon( + isDestinationLibrary ? libraryIcon ?? item.activeIcon : item.activeIcon, + ), + label: isDestinationLibrary ? currentLibrary?.name ?? item.name : item.name, + tooltip: item.tooltip, + ); + if (isDestinationLibrary) { + return GestureDetector( + onSecondaryTap: () => showLibrarySwitcher(context, ref), + onDoubleTap: () => showLibrarySwitcher(context, ref), + child: destinationWidget, // Wrap the actual NavigationDestination + ); + } else { + // Return the unwrapped destination for other items + return destinationWidget; + } + }).toList(), + selectedIndex: navigationShell.currentIndex, + onDestinationSelected: (int index) => _onTap(context, index, ref), ), ); } diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index b3c1761..d4b56a3 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -10,4 +10,14 @@ class AppDelegate: FlutterAppDelegate { override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { return true } + + override func applicationDidFinishLaunching(_ notification: Notification) { + guard let window = NSApplication.shared.windows.first else { + return + } + window.setContentSize(NSSize(width: 1280, height: 720)) // 设置初始大小 + // window.minSize = NSSize(width: 640, height: 480) // 最小尺寸 + // window?.maxSize = NSSize(width: 1200, height: 900) // 最大尺寸(可选) + window.setFrameOrigin(NSPoint(x: 50, y: NSScreen.main?.frame.height ?? 1080 - (window.frame.height - 50))) + } }