非移动端修改为左侧导航

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
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,
)
.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

View file

@ -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(

View file

@ -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),
),
);
}

View file

@ -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)))
}
}