非移动端修改为左侧导航

This commit is contained in:
rang 2025-10-20 14:20:11 +08:00
parent 87d15c71d1
commit 0b71777b41
5 changed files with 213 additions and 73 deletions

View file

@ -235,3 +235,57 @@ class AudiobookChapterProgressBar extends HookConsumerWidget {
// ! TODO remove onTap // ! TODO remove onTap
void 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 }

View file

@ -43,9 +43,16 @@ class PlayerWhenExpanded extends HookConsumerWidget {
earlyEnd, earlyEnd,
) )
.clamp(0.0, 1.0); .clamp(0.0, 1.0);
final currentChapter = ref.watch(currentPlayingChapterProvider); final currentChapter = ref.watch(currentPlayingChapterProvider);
final currentBookMetadata = ref.watch(currentBookMetadataProvider); 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( return Column(
children: [ children: [
// sized box for system status bar; not needed as not full screen // sized box for system status bar; not needed as not full screen
@ -128,9 +135,9 @@ class PlayerWhenExpanded extends HookConsumerWidget {
), ),
// the chapter title // the chapter title
if (earlyPercentage > 0.4) if (chapterPercentage >= 1)
Opacity( Opacity(
opacity: earlyPercentage, opacity: chapterPercentage,
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: AppElementSizes.paddingRegular * 4 * earlyPercentage, top: AppElementSizes.paddingRegular * 4 * earlyPercentage,
@ -152,9 +159,9 @@ class PlayerWhenExpanded extends HookConsumerWidget {
), ),
// the book name and author // the book name and author
if (earlyPercentage > 0.5) if (authorPercentage >= 1)
Opacity( Opacity(
opacity: earlyPercentage, opacity: authorPercentage,
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: AppElementSizes.paddingRegular * earlyPercentage, 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 // the progress bar
if (earlyPercentage > 0.6) if (progressPercentage >= 1)
Opacity( Opacity(
opacity: earlyPercentage, opacity: progressPercentage,
child: SizedBox( child: SizedBox(
width: imageSize, width: imageSize,
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: AppElementSizes.paddingRegular * earlyPercentage, // top: AppElementSizes.paddingRegular * earlyPercentage,
left: AppElementSizes.paddingRegular * earlyPercentage, left: AppElementSizes.paddingRegular * earlyPercentage,
right: 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 // the chapter skip buttons, seek 30 seconds back and forward, and play/pause button
if (earlyPercentage > 0.8) if (playPercentage >= 1)
Opacity( Opacity(
opacity: earlyPercentage, opacity: playPercentage,
child: SizedBox( child: SizedBox(
width: imageSize, width: imageSize,
child: Row( 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 // speed control, sleep timer, chapter list, and settings
if (earlyPercentage > 0.9) if (settingsPercentage >= 1)
Opacity( Opacity(
opacity: earlyPercentage, opacity: settingsPercentage,
child: Padding( child: SizedBox(
padding: EdgeInsets.only( // padding: EdgeInsets.only(
bottom: AppElementSizes.paddingRegular * 4 * earlyPercentage, // bottom: AppElementSizes.paddingRegular * 4 * earlyPercentage,
), // ),
width: imageSize,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
// speed control // speed control
const PlayerSpeedAdjustButton(), const PlayerSpeedAdjustButton(),
const Spacer(),
// sleep timer // sleep timer
const SleepTimerButton(), const SleepTimerButton(),
const Spacer(),
// chapter list // chapter list
const ChapterSelectionButton(), const ChapterSelectionButton(),
// settings // settings

View file

@ -37,8 +37,12 @@ class PlayerWhenMinimized extends HookConsumerWidget {
final bookMetaExpanded = ref.watch(currentBookMetadataProvider); final bookMetaExpanded = ref.watch(currentBookMetadataProvider);
var barHeight = vanishingPercentage * 3; var barHeight = vanishingPercentage * 3;
final adjuster = ProportionalAdjuster();
final rewindPercentage = adjuster.adjust(vanishingPercentage, 1.5);
final playPercentage = adjuster.adjust(vanishingPercentage, 1.8);
return Stack( return Stack(
alignment: Alignment.bottomCenter, alignment: Alignment.topCenter,
children: [ children: [
Row( Row(
children: [ children: [
@ -100,11 +104,10 @@ class PlayerWhenMinimized extends HookConsumerWidget {
// controller.animateToHeight(state: PanelState.MAX); // controller.animateToHeight(state: PanelState.MAX);
// }, // },
// ), // ),
// rewind button // rewind button
if (vanishingPercentage > 0.6) if (rewindPercentage >= 1)
Opacity( Opacity(
opacity: vanishingPercentage, opacity: rewindPercentage,
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 8), padding: const EdgeInsets.only(left: 8),
child: IconButton( child: IconButton(
@ -117,9 +120,9 @@ class PlayerWhenMinimized extends HookConsumerWidget {
), ),
), ),
// play/pause button // play/pause button
if (vanishingPercentage > 0.8) if (playPercentage >= 1)
Opacity( Opacity(
opacity: vanishingPercentage, opacity: playPercentage,
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 8), padding: const EdgeInsets.only(right: 8),
child: AudiobookPlayerPlayPauseButton( child: AudiobookPlayerPlayPauseButton(
@ -129,6 +132,7 @@ class PlayerWhenMinimized extends HookConsumerWidget {
), ),
], ],
), ),
const Spacer(),
SizedBox( SizedBox(
height: barHeight, height: barHeight,
child: LinearProgressIndicator( child: LinearProgressIndicator(

View file

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.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';
@ -33,16 +35,8 @@ class ScaffoldWithNavBar extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { 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 playerProgress = ref.watch(playerHeightProvider);
final playerMaxHeight = MediaQuery.of(context).size.height; final isMobile = Platform.isAndroid || Platform.isIOS;
var percentExpandedMiniPlayer = (playerProgress - playerMinHeight) /
(playerMaxHeight - playerMinHeight);
// Clamp the value between 0 and 1
percentExpandedMiniPlayer = percentExpandedMiniPlayer.clamp(0.0, 1.0);
onBackButtonPressed() async { onBackButtonPressed() async {
final isPlayerExpanded = playerProgress != playerMinHeight; final isPlayerExpanded = playerProgress != playerMinHeight;
@ -96,61 +90,129 @@ class ScaffoldWithNavBar extends HookConsumerWidget {
return BackButtonListener( return BackButtonListener(
onBackButtonPressed: onBackButtonPressed, onBackButtonPressed: onBackButtonPressed,
child: Scaffold( child: Scaffold(
body: Stack( body: isMobile
children: [ ? Stack(
navigationShell, children: [
const AudiobookPlayer(), navigationShell,
], const AudiobookPlayer(),
), ],
bottomNavigationBar: Opacity( )
// Opacity is interpolated from 1 to 0 when player is expanded : buildNavLeft(context, ref),
opacity: 1 - percentExpandedMiniPlayer, bottomNavigationBar: isMobile ? buildNavBottom(context, ref) : null,
child: NavigationBar( ),
elevation: 0.0, );
height: bottomBarHeight * (1 - percentExpandedMiniPlayer), }
// TODO: get destinations from the navigationShell Widget buildNavLeft(BuildContext context, WidgetRef ref) {
// Here, the items of BottomNavigationBar are hard coded. In a real return Row(
// world scenario, the items would most likely be generated from the children: [
// branches of the shell route, which can be fetched using SafeArea(
// `navigationShell.route.branches`. child: NavigationRail(
minWidth: 60,
minExtendedWidth: 120,
extended: MediaQuery.of(context).size.width > 640,
// extended: false,
destinations: _navigationItems.map((item) { destinations: _navigationItems.map((item) {
final isDestinationLibrary = item.name == 'Library'; final isDestinationLibrary = item.name == 'Library';
var currentLibrary = var currentLibrary = ref.watch(currentLibraryProvider).valueOrNull;
ref.watch(currentLibraryProvider).valueOrNull;
final libraryIcon = AbsIcons.getIconByName( final libraryIcon = AbsIcons.getIconByName(
currentLibrary?.icon, currentLibrary?.icon,
); );
final destinationWidget = NavigationDestination( final destinationWidget = NavigationRailDestination(
icon: Icon( icon: Icon(
isDestinationLibrary ? libraryIcon ?? item.icon : item.icon, isDestinationLibrary ? libraryIcon ?? item.icon : item.icon,
), ),
selectedIcon: Icon( selectedIcon: Icon(
isDestinationLibrary isDestinationLibrary ? libraryIcon ?? item.activeIcon : item.activeIcon,
? libraryIcon ?? item.activeIcon
: item.activeIcon,
), ),
label: isDestinationLibrary label: Text(isDestinationLibrary ? currentLibrary?.name ?? item.name : item.name),
? currentLibrary?.name ?? item.name // tooltip: item.tooltip,
: item.name,
tooltip: item.tooltip,
); );
if (isDestinationLibrary) { // if (isDestinationLibrary) {
return GestureDetector( // return GestureDetector(
onSecondaryTap: () => showLibrarySwitcher(context, ref), // onSecondaryTap: () => showLibrarySwitcher(context, ref),
onDoubleTap: () => showLibrarySwitcher(context, ref), // onDoubleTap: () => showLibrarySwitcher(context, ref),
child: // child:
destinationWidget, // Wrap the actual NavigationDestination // destinationWidget, // Wrap the actual NavigationDestination
); // );
} else { // } else {
// Return the unwrapped destination for other items // // Return the unwrapped destination for other items
return destinationWidget; // return destinationWidget;
} // }
return destinationWidget;
// return NavigationRailDestination(icon: Icon(nav.icon), label: Text(nav.name));
}).toList(), }).toList(),
selectedIndex: navigationShell.currentIndex, 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),
), ),
); );
} }

View file

@ -10,4 +10,14 @@ class AppDelegate: FlutterAppDelegate {
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true 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)))
}
} }