mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2026-02-16 14:29:35 +00:00
非移动端修改为左侧导航
This commit is contained in:
parent
87d15c71d1
commit
0b71777b41
5 changed files with 213 additions and 73 deletions
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue