mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-09 07:28:14 -07:00
Init repo
This commit is contained in:
commit
764b6034e3
566 changed files with 212335 additions and 0 deletions
50
lib/widgets/navigation_scaffold/components/adaptive_fab.dart
Normal file
50
lib/widgets/navigation_scaffold/components/adaptive_fab.dart
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class AdaptiveFab {
|
||||
final BuildContext context;
|
||||
final String title;
|
||||
final Widget child;
|
||||
final Function() onPressed;
|
||||
final Key? key;
|
||||
AdaptiveFab({
|
||||
required this.context,
|
||||
this.title = '',
|
||||
required this.child,
|
||||
required this.onPressed,
|
||||
this.key,
|
||||
});
|
||||
|
||||
FloatingActionButton get normal {
|
||||
return FloatingActionButton(
|
||||
key: key,
|
||||
onPressed: onPressed,
|
||||
tooltip: title,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget get extended {
|
||||
return AnimatedContainer(
|
||||
key: key,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
height: 60,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
child,
|
||||
const Spacer(),
|
||||
Flexible(child: Text(title)),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import 'package:fladder/routes/build_routes/route_builder.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/navigation_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DestinationModel {
|
||||
final String label;
|
||||
final Widget? icon;
|
||||
final Widget? selectedIcon;
|
||||
final CustomRoute? route;
|
||||
final Function()? action;
|
||||
final String? tooltip;
|
||||
final Badge? badge;
|
||||
final AdaptiveFab? floatingActionButton;
|
||||
// final FloatingActionButton? floatingActionButton;
|
||||
|
||||
DestinationModel({
|
||||
required this.label,
|
||||
this.icon,
|
||||
this.selectedIcon,
|
||||
this.route,
|
||||
this.action,
|
||||
this.tooltip,
|
||||
this.badge,
|
||||
this.floatingActionButton,
|
||||
}) : assert(
|
||||
badge == null || icon == null,
|
||||
'Only one of icon or badge should be provided, not both.',
|
||||
);
|
||||
|
||||
/// Converts this [DestinationModel] to a [NavigationRailDestination] used in a [NavigationRail].
|
||||
NavigationRailDestination toNavigationRailDestination({EdgeInsets? padding}) {
|
||||
if (badge != null) {
|
||||
return NavigationRailDestination(
|
||||
icon: badge!,
|
||||
label: Text(label),
|
||||
selectedIcon: badge!,
|
||||
padding: padding,
|
||||
);
|
||||
}
|
||||
return NavigationRailDestination(
|
||||
icon: icon!,
|
||||
label: Text(label),
|
||||
selectedIcon: selectedIcon,
|
||||
padding: padding,
|
||||
);
|
||||
}
|
||||
|
||||
/// Converts this [DestinationModel] to a [NavigationDrawerDestination] used in a [NavigationDrawer].
|
||||
NavigationDrawerDestination toNavigationDrawerDestination() {
|
||||
if (badge != null) {
|
||||
return NavigationDrawerDestination(
|
||||
icon: badge!,
|
||||
label: Text(label),
|
||||
selectedIcon: badge!,
|
||||
);
|
||||
}
|
||||
return NavigationDrawerDestination(
|
||||
icon: icon!,
|
||||
label: Text(label),
|
||||
selectedIcon: selectedIcon,
|
||||
);
|
||||
}
|
||||
|
||||
/// Converts this [DestinationModel] to a [NavigationDestination] used in a [BottomNavigationBar].
|
||||
NavigationDestination toNavigationDestination() {
|
||||
if (badge != null) {
|
||||
return NavigationDestination(
|
||||
icon: badge!,
|
||||
label: label,
|
||||
selectedIcon: badge!,
|
||||
);
|
||||
}
|
||||
return NavigationDestination(
|
||||
icon: icon!,
|
||||
label: label,
|
||||
selectedIcon: selectedIcon,
|
||||
tooltip: tooltip,
|
||||
);
|
||||
}
|
||||
|
||||
NavigationButton toNavigationButton(bool selected, bool expanded) {
|
||||
return NavigationButton(
|
||||
label: label,
|
||||
selected: selected,
|
||||
onPressed: action,
|
||||
horizontal: expanded,
|
||||
selectedIcon: selectedIcon!,
|
||||
icon: icon!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class DrawerListButton extends ConsumerStatefulWidget {
|
||||
final String label;
|
||||
final Widget selectedIcon;
|
||||
final Widget icon;
|
||||
final Function()? onPressed;
|
||||
final List<ItemAction> actions;
|
||||
final bool selected;
|
||||
final Duration duration;
|
||||
const DrawerListButton({
|
||||
required this.label,
|
||||
required this.selectedIcon,
|
||||
required this.icon,
|
||||
this.onPressed,
|
||||
this.actions = const [],
|
||||
this.selected = false,
|
||||
this.duration = const Duration(milliseconds: 125),
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _DrawerListButtonState();
|
||||
}
|
||||
|
||||
class _DrawerListButtonState extends ConsumerState<DrawerListButton> {
|
||||
bool showPopupButton = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (event) => setState(() => showPopupButton = true),
|
||||
onExit: (event) => setState(() => showPopupButton = false),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: ListTile(
|
||||
onTap: widget.onPressed,
|
||||
horizontalTitleGap: 15,
|
||||
selected: widget.selected,
|
||||
selectedTileColor: Theme.of(context).colorScheme.primary,
|
||||
selectedColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onLongPress: widget.actions.isNotEmpty && AdaptiveLayout.of(context).inputDevice == InputDevice.touch
|
||||
? () => showBottomSheetPill(
|
||||
context: context,
|
||||
content: (context, scrollController) => ListView(
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
children: widget.actions.listTileItems(context, useIcons: true),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 5),
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.all(3),
|
||||
child:
|
||||
AnimatedFadeSize(duration: widget.duration, child: widget.selected ? widget.selectedIcon : widget.icon),
|
||||
),
|
||||
trailing: widget.actions.isNotEmpty && AdaptiveLayout.of(context).inputDevice == InputDevice.pointer
|
||||
? AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 125),
|
||||
opacity: showPopupButton ? 1 : 0,
|
||||
child: PopupMenuButton(
|
||||
tooltip: "Options",
|
||||
itemBuilder: (context) => widget.actions.popupMenuItems(useIcons: true),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
title: Text(widget.label),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import 'package:fladder/screens/shared/default_titlebar.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
bool get _isDesktop {
|
||||
if (kIsWeb) return false;
|
||||
return [
|
||||
TargetPlatform.windows,
|
||||
TargetPlatform.linux,
|
||||
TargetPlatform.macOS,
|
||||
].contains(defaultTargetPlatform);
|
||||
}
|
||||
|
||||
class FladderAppbar extends StatelessWidget implements PreferredSize {
|
||||
final double height;
|
||||
final String? label;
|
||||
final bool automaticallyImplyLeading;
|
||||
const FladderAppbar({this.height = 35, this.automaticallyImplyLeading = false, this.label, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (AdaptiveLayout.of(context).isDesktop) {
|
||||
return PreferredSize(
|
||||
preferredSize: Size(double.infinity, height),
|
||||
child: SizedBox(
|
||||
height: height,
|
||||
child: Row(
|
||||
children: [
|
||||
if (automaticallyImplyLeading && context.canPop()) BackButton(),
|
||||
Expanded(
|
||||
child: DefaultTitleBar(
|
||||
label: label,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
));
|
||||
} else {
|
||||
return AppBar(
|
||||
toolbarHeight: 0,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface.withOpacity(0),
|
||||
scrolledUnderElevation: 0,
|
||||
elevation: 0,
|
||||
systemOverlayStyle: SystemUiOverlayStyle(),
|
||||
title: const Text(""),
|
||||
automaticallyImplyLeading: automaticallyImplyLeading,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget get child => Container();
|
||||
|
||||
@override
|
||||
Size get preferredSize => Size(double.infinity, _isDesktop ? height : 0);
|
||||
}
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
import 'package:ficonsax/ficonsax.dart';
|
||||
import 'package:fladder/models/media_playback_model.dart';
|
||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||
import 'package:fladder/providers/video_player_provider.dart';
|
||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/video_player/video_player.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/duration_extensions.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/refresh_state.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class FloatingPlayerBar extends ConsumerStatefulWidget {
|
||||
const FloatingPlayerBar({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _CurrentlyPlayingBarState();
|
||||
}
|
||||
|
||||
class _CurrentlyPlayingBarState extends ConsumerState<FloatingPlayerBar> {
|
||||
bool showExpandButton = false;
|
||||
|
||||
Future<void> openFullScreenPlayer() async {
|
||||
setState(() => showExpandButton = false);
|
||||
ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(state: VideoPlayerState.fullScreen));
|
||||
await Navigator.of(context, rootNavigator: true).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const VideoPlayer(),
|
||||
),
|
||||
);
|
||||
if (AdaptiveLayout.of(context).isDesktop || kIsWeb) {
|
||||
final fullScreen = await windowManager.isFullScreen();
|
||||
if (fullScreen) {
|
||||
await windowManager.setFullScreen(false);
|
||||
}
|
||||
}
|
||||
if (context.mounted) {
|
||||
context.refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopPlayer() async {
|
||||
ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(state: VideoPlayerState.disposed));
|
||||
return ref.read(videoPlayerProvider).stop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final playbackInfo = ref.watch(mediaPlaybackProvider);
|
||||
final player = ref.watch(videoPlayerProvider);
|
||||
final playbackModel = ref.watch(playBackModel.select((value) => value?.item));
|
||||
final progress = playbackInfo.position.inMilliseconds / playbackInfo.duration.inMilliseconds;
|
||||
return Dismissible(
|
||||
key: Key("CurrentlyPlayingBar"),
|
||||
confirmDismiss: (direction) async {
|
||||
if (direction == DismissDirection.up) {
|
||||
await openFullScreenPlayer();
|
||||
} else {
|
||||
await stopPlayer();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
direction: DismissDirection.vertical,
|
||||
child: InkWell(
|
||||
onLongPress: () {
|
||||
fladderSnackbar(context, title: "Swipe up/down to open/close the player");
|
||||
},
|
||||
child: Card(
|
||||
elevation: 3,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: 50, maxHeight: 85),
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: [
|
||||
if (playbackInfo.state == VideoPlayerState.minimized)
|
||||
Card(
|
||||
child: SizedBox(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.67,
|
||||
child: MouseRegion(
|
||||
onEnter: (event) => setState(() => showExpandButton = true),
|
||||
onExit: (event) => setState(() => showExpandButton = false),
|
||||
child: Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: "HeroPlayer",
|
||||
child: Video(
|
||||
controller: player.controller!,
|
||||
fit: BoxFit.fitHeight,
|
||||
controls: NoVideoControls,
|
||||
wakelock: playbackInfo.playing,
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Tooltip(
|
||||
message: "Expand player",
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
child: AnimatedOpacity(
|
||||
opacity: showExpandButton ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 125),
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
child: FlatButton(
|
||||
onTap: () async => openFullScreenPlayer(),
|
||||
child: Icon(Icons.keyboard_arrow_up_rounded),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
playbackModel?.title ?? "",
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
if (playbackModel?.detailedName(context)?.isNotEmpty == true)
|
||||
Flexible(
|
||||
child: Text(
|
||||
playbackModel?.detailedName(context) ?? "",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!progress.isNaN && constraints.maxWidth > 500)
|
||||
Text(
|
||||
"${playbackInfo.position.readAbleDuration} / ${playbackInfo.duration.readAbleDuration}"),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: IconButton.filledTonal(
|
||||
onPressed: () => ref.read(videoPlayerProvider).playOrPause(),
|
||||
icon: playbackInfo.playing
|
||||
? Icon(Icons.pause_rounded)
|
||||
: Icon(Icons.play_arrow_rounded),
|
||||
),
|
||||
),
|
||||
if (constraints.maxWidth > 500) ...{
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
final volume = player.player?.state.volume == 0 ? 100.0 : 0.0;
|
||||
ref.read(videoPlayerSettingsProvider.notifier).setVolume(volume);
|
||||
player.setVolume(volume);
|
||||
},
|
||||
icon: Icon(
|
||||
ref.watch(videoPlayerSettingsProvider.select((value) => value.volume)) <= 0
|
||||
? IconsaxBold.volume_cross
|
||||
: IconsaxBold.volume_high,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: "Stop playback",
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
child: IconButton(
|
||||
onPressed: () async => stopPlayer(),
|
||||
icon: Icon(IconsaxBold.stop),
|
||||
),
|
||||
),
|
||||
},
|
||||
].addInBetween(SizedBox(width: 8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
LinearProgressIndicator(
|
||||
minHeight: 6,
|
||||
backgroundColor: Colors.black.withOpacity(0.25),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
value: progress.clamp(0, 1),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
170
lib/widgets/navigation_scaffold/components/navigation_body.dart
Normal file
170
lib/widgets/navigation_scaffold/components/navigation_body.dart
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import 'package:ficonsax/ficonsax.dart';
|
||||
import 'package:fladder/providers/views_provider.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/navigation_drawer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/routes/build_routes/settings_routes.dart';
|
||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart';
|
||||
|
||||
class NavigationBody extends ConsumerStatefulWidget {
|
||||
final BuildContext parentContext;
|
||||
final Widget child;
|
||||
final int currentIndex;
|
||||
final List<DestinationModel> destinations;
|
||||
final String currentLocation;
|
||||
final GlobalKey<ScaffoldState> drawerKey;
|
||||
const NavigationBody({
|
||||
required this.parentContext,
|
||||
required this.child,
|
||||
required this.currentIndex,
|
||||
required this.destinations,
|
||||
required this.currentLocation,
|
||||
required this.drawerKey,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _NavigationBodyState();
|
||||
}
|
||||
|
||||
class _NavigationBodyState extends ConsumerState<NavigationBody> {
|
||||
bool expandedSideBar = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((value) {
|
||||
ref.read(viewsProvider.notifier).fetchViews();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final views = ref.watch(viewsProvider.select((value) => value.views));
|
||||
return switch (AdaptiveLayout.layoutOf(context)) {
|
||||
LayoutState.phone => MediaQuery.removePadding(
|
||||
context: widget.parentContext,
|
||||
child: widget.child,
|
||||
),
|
||||
LayoutState.tablet => Row(
|
||||
children: [
|
||||
navigationRail(context),
|
||||
Flexible(
|
||||
child: widget.child,
|
||||
)
|
||||
],
|
||||
),
|
||||
LayoutState.desktop => Row(
|
||||
children: [
|
||||
AnimatedFadeSize(
|
||||
duration: const Duration(milliseconds: 125),
|
||||
child: expandedSideBar
|
||||
? MediaQuery.removePadding(
|
||||
context: widget.parentContext,
|
||||
child: NestedNavigationDrawer(
|
||||
isExpanded: expandedSideBar,
|
||||
actionButton: actionButton(),
|
||||
toggleExpanded: (value) {
|
||||
setState(() {
|
||||
expandedSideBar = value;
|
||||
});
|
||||
},
|
||||
views: views,
|
||||
destinations: widget.destinations,
|
||||
currentLocation: widget.currentLocation,
|
||||
),
|
||||
)
|
||||
: navigationRail(context),
|
||||
),
|
||||
Flexible(
|
||||
child: widget.child,
|
||||
),
|
||||
],
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
AdaptiveFab? actionButton() {
|
||||
return (widget.currentIndex >= 0 && widget.currentIndex < widget.destinations.length)
|
||||
? widget.destinations[widget.currentIndex].floatingActionButton
|
||||
: null;
|
||||
}
|
||||
|
||||
Widget navigationRail(BuildContext context) {
|
||||
return Padding(
|
||||
key: const Key('navigation_rail'),
|
||||
padding: AdaptiveLayout.of(context).isDesktop ? EdgeInsets.zero : MediaQuery.of(context).padding,
|
||||
child: Column(
|
||||
children: [
|
||||
if (AdaptiveLayout.of(context).isDesktop && AdaptiveLayout.of(context).platform != TargetPlatform.macOS) ...{
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"Fladder",
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
},
|
||||
const SizedBox(height: 8),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (AdaptiveLayout.layoutOf(context) != LayoutState.desktop) {
|
||||
widget.drawerKey.currentState?.openDrawer();
|
||||
} else {
|
||||
setState(() {
|
||||
expandedSideBar = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
icon: const Icon(IconsaxBold.menu),
|
||||
),
|
||||
if (AdaptiveLayout.of(context).isDesktop) ...[
|
||||
const SizedBox(height: 8),
|
||||
AnimatedFadeSize(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(scale: animation, child: child);
|
||||
},
|
||||
child: actionButton()?.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
IconTheme(
|
||||
data: IconThemeData(size: 28),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
...widget.destinations.mapIndexed(
|
||||
(index, destination) => destination.toNavigationButton(widget.currentIndex == index, false),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: widget.currentLocation.contains(SettingsRoute().route)
|
||||
? Card(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Icon(IconsaxBold.setting_3),
|
||||
),
|
||||
)
|
||||
: const SettingsUserIcon()),
|
||||
),
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
import 'package:fladder/util/widget_extensions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class NavigationButton extends ConsumerStatefulWidget {
|
||||
final String? label;
|
||||
final Widget selectedIcon;
|
||||
final Widget icon;
|
||||
final bool horizontal;
|
||||
final Function()? onPressed;
|
||||
final bool selected;
|
||||
final Duration duration;
|
||||
const NavigationButton({
|
||||
required this.label,
|
||||
required this.selectedIcon,
|
||||
required this.icon,
|
||||
this.horizontal = false,
|
||||
this.onPressed,
|
||||
this.selected = false,
|
||||
this.duration = const Duration(milliseconds: 125),
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _NavigationButtonState();
|
||||
}
|
||||
|
||||
class _NavigationButtonState extends ConsumerState<NavigationButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
waitDuration: const Duration(seconds: 1),
|
||||
preferBelow: false,
|
||||
triggerMode: TooltipTriggerMode.longPress,
|
||||
message: widget.label ?? "",
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: widget.horizontal
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: getChildren(context),
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: getChildren(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> getChildren(BuildContext context) {
|
||||
return [
|
||||
Flexible(
|
||||
child: ElevatedButton(
|
||||
style: ButtonStyle(
|
||||
elevation: WidgetStatePropertyAll(0),
|
||||
padding: WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
backgroundColor: WidgetStatePropertyAll(Colors.transparent),
|
||||
foregroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
return widget.selected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurface.withOpacity(0.45);
|
||||
})),
|
||||
onPressed: widget.onPressed,
|
||||
child: AnimatedContainer(
|
||||
curve: Curves.fastOutSlowIn,
|
||||
duration: widget.duration,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: widget.duration,
|
||||
child: widget.selected
|
||||
? widget.selectedIcon.setKey(Key("${widget.label}+selected"))
|
||||
: widget.icon.setKey(Key("${widget.label}+normal")),
|
||||
),
|
||||
if (widget.horizontal && widget.label != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: _Label(widget: widget),
|
||||
)
|
||||
],
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: Duration(milliseconds: 250),
|
||||
margin: EdgeInsets.only(top: widget.selected ? 8 : 0),
|
||||
height: widget.selected ? 6 : 0,
|
||||
width: widget.selected ? 14 : 0,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(widget.selected ? 1 : 0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _Label extends StatelessWidget {
|
||||
const _Label({required this.widget});
|
||||
|
||||
final NavigationButton widget;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
widget.label!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.fade,
|
||||
style:
|
||||
Theme.of(context).textTheme.labelMedium?.copyWith(color: Theme.of(context).colorScheme.onSecondaryContainer),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
import 'package:ficonsax/ficonsax.dart';
|
||||
import 'package:fladder/models/collection_types.dart';
|
||||
import 'package:fladder/models/view_model.dart';
|
||||
import 'package:fladder/routes/build_routes/home_routes.dart';
|
||||
import 'package:fladder/routes/build_routes/route_builder.dart';
|
||||
import 'package:fladder/routes/build_routes/settings_routes.dart';
|
||||
import 'package:fladder/screens/metadata/refresh_metadata.dart';
|
||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/drawer_list_button.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class NestedNavigationDrawer extends ConsumerWidget {
|
||||
final bool isExpanded;
|
||||
final Function(bool expanded) toggleExpanded;
|
||||
final List<DestinationModel> destinations;
|
||||
final AdaptiveFab? actionButton;
|
||||
final List<ViewModel> views;
|
||||
final String currentLocation;
|
||||
const NestedNavigationDrawer(
|
||||
{this.isExpanded = false,
|
||||
required this.toggleExpanded,
|
||||
required this.actionButton,
|
||||
required this.destinations,
|
||||
required this.views,
|
||||
required this.currentLocation,
|
||||
super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return NavigationDrawer(
|
||||
key: const Key('navigation_drawer'),
|
||||
backgroundColor: isExpanded ? Colors.transparent : null,
|
||||
surfaceTintColor: isExpanded ? Colors.transparent : null,
|
||||
children: [
|
||||
if (AdaptiveLayout.of(context).isDesktop || kIsWeb) const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(28, 0, 16, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.localized.navigation,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => toggleExpanded(false),
|
||||
icon: const Icon(IconsaxOutline.menu),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: actionButton != null ? 8 : 0),
|
||||
child: AnimatedFadeSize(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(scale: animation, child: child);
|
||||
},
|
||||
child: actionButton?.extended,
|
||||
),
|
||||
),
|
||||
),
|
||||
...destinations.map((destination) => DrawerListButton(
|
||||
label: destination.label,
|
||||
selected: destination.route?.route == currentLocation,
|
||||
selectedIcon: destination.selectedIcon!,
|
||||
icon: destination.icon!,
|
||||
onPressed: () {
|
||||
destination.action!();
|
||||
Scaffold.of(context).closeDrawer();
|
||||
},
|
||||
)),
|
||||
if (views.isNotEmpty) ...{
|
||||
const Divider(indent: 28, endIndent: 28),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(28, 16, 16, 10),
|
||||
child: Text(
|
||||
context.localized.library(2),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
...views.map((library) => DrawerListButton(
|
||||
label: library.name,
|
||||
selected: currentLocation.contains(library.id),
|
||||
actions: [
|
||||
ItemActionButton(
|
||||
label: Text(context.localized.scanLibrary),
|
||||
icon: Icon(IconsaxOutline.refresh),
|
||||
action: () => showRefreshPopup(context, library.id, library.name),
|
||||
),
|
||||
],
|
||||
onPressed: () {
|
||||
context.routePushOrGo(LibrarySearchRoute(id: library.id));
|
||||
Scaffold.of(context).closeDrawer();
|
||||
},
|
||||
selectedIcon: Icon(library.collectionType.icon),
|
||||
icon: Icon(library.collectionType.iconOutlined))),
|
||||
},
|
||||
const Divider(indent: 28, endIndent: 28),
|
||||
if (isExpanded)
|
||||
Transform.translate(
|
||||
offset: Offset(-8, 0),
|
||||
child: DrawerListButton(
|
||||
label: context.localized.settings,
|
||||
selectedIcon: const Icon(IconsaxBold.setting_3),
|
||||
selected: currentLocation.contains(SettingsRoute().basePath),
|
||||
icon: const SizedBox(width: 35, height: 35, child: SettingsUserIcon()),
|
||||
onPressed: () {
|
||||
switch (AdaptiveLayout.of(context).size) {
|
||||
case ScreenLayout.single:
|
||||
context.routePush(SettingsRoute());
|
||||
break;
|
||||
case ScreenLayout.dual:
|
||||
context.routeGo(ClientSettingsRoute());
|
||||
break;
|
||||
}
|
||||
Scaffold.of(context).closeDrawer();
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
DrawerListButton(
|
||||
label: context.localized.settings,
|
||||
selectedIcon: Icon(IconsaxBold.setting_2),
|
||||
icon: Icon(IconsaxOutline.setting_2),
|
||||
selected: currentLocation.contains(SettingsRoute().basePath),
|
||||
onPressed: () {
|
||||
switch (AdaptiveLayout.of(context).size) {
|
||||
case ScreenLayout.single:
|
||||
context.routePush(SettingsRoute());
|
||||
break;
|
||||
case ScreenLayout.dual:
|
||||
context.routeGo(ClientSettingsRoute());
|
||||
break;
|
||||
}
|
||||
Scaffold.of(context).closeDrawer();
|
||||
},
|
||||
),
|
||||
if (AdaptiveLayout.of(context).isDesktop || kIsWeb) const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/routes/build_routes/route_builder.dart';
|
||||
import 'package:fladder/routes/build_routes/settings_routes.dart';
|
||||
import 'package:fladder/screens/shared/user_icon.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class SettingsUserIcon extends ConsumerWidget {
|
||||
const SettingsUserIcon({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final users = ref.watch(userProvider);
|
||||
return Tooltip(
|
||||
message: context.localized.settings,
|
||||
waitDuration: const Duration(seconds: 1),
|
||||
child: UserIcon(
|
||||
user: users,
|
||||
cornerRadius: 200,
|
||||
onLongPress: () => context.routePush(LockScreenRoute()),
|
||||
onTap: () => switch (AdaptiveLayout.of(context).size) {
|
||||
ScreenLayout.single => context.routePush(SettingsRoute()),
|
||||
ScreenLayout.dual => context.routePush(ClientSettingsRoute()),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
118
lib/widgets/navigation_scaffold/navigation_scaffold.dart
Normal file
118
lib/widgets/navigation_scaffold/navigation_scaffold.dart
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import 'package:fladder/models/media_playback_model.dart';
|
||||
import 'package:fladder/providers/video_player_provider.dart';
|
||||
import 'package:fladder/providers/views_provider.dart';
|
||||
import 'package:fladder/routes/app_routes.dart';
|
||||
import 'package:fladder/screens/shared/nested_bottom_appbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/fladder_appbar.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/navigation_body.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/navigation_drawer.dart';
|
||||
import 'package:fladder/widgets/shared/hide_on_scroll.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class NavigationScaffold extends ConsumerStatefulWidget {
|
||||
final int? currentIndex;
|
||||
final String? location;
|
||||
final Widget? nestedChild;
|
||||
final List<DestinationModel> destinations;
|
||||
final GlobalKey<NavigatorState>? nestedNavigatorKey;
|
||||
const NavigationScaffold({
|
||||
this.currentIndex,
|
||||
this.location,
|
||||
this.nestedChild,
|
||||
required this.destinations,
|
||||
this.nestedNavigatorKey,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _NavigationScaffoldState();
|
||||
}
|
||||
|
||||
class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
|
||||
final GlobalKey<ScaffoldState> _key = GlobalKey();
|
||||
|
||||
int get currentIndex => widget.destinations.indexWhere((element) => element.route?.route == widget.location);
|
||||
String get currentLocation => widget.location ?? "Nothing";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((value) {
|
||||
ref.read(viewsProvider.notifier).fetchViews();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state));
|
||||
final views = ref.watch(viewsProvider.select((value) => value.views));
|
||||
return PopScope(
|
||||
canPop: currentIndex == 0,
|
||||
onPopInvoked: (didPop) {
|
||||
if (currentIndex != 0) {
|
||||
widget.destinations.first.action!();
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
key: _key,
|
||||
appBar: const FladderAppbar(),
|
||||
extendBodyBehindAppBar: true,
|
||||
extendBody: true,
|
||||
floatingActionButtonLocation:
|
||||
playerState == VideoPlayerState.minimized ? FloatingActionButtonLocation.centerFloat : null,
|
||||
floatingActionButton: AdaptiveLayout.of(context).layout == LayoutState.phone
|
||||
? switch (playerState) {
|
||||
VideoPlayerState.minimized => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: FloatingPlayerBar(),
|
||||
),
|
||||
_ => widget.destinations.elementAtOrNull(currentIndex)?.floatingActionButton?.normal,
|
||||
}
|
||||
: null,
|
||||
drawer: NestedNavigationDrawer(
|
||||
actionButton: null,
|
||||
toggleExpanded: (value) {
|
||||
_key.currentState?.closeDrawer();
|
||||
},
|
||||
views: views,
|
||||
destinations: widget.destinations,
|
||||
currentLocation: currentLocation,
|
||||
),
|
||||
bottomNavigationBar: AdaptiveLayout.of(context).layout == LayoutState.phone
|
||||
? HideOnScroll(
|
||||
controller: AppRoutes.scrollController,
|
||||
child: NestedBottomAppBar(
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: widget.destinations
|
||||
.map(
|
||||
(destination) =>
|
||||
destination.toNavigationButton(widget.location == destination.route?.route, false),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
body: widget.nestedChild != null
|
||||
? NavigationBody(
|
||||
child: widget.nestedChild!,
|
||||
parentContext: context,
|
||||
currentIndex: currentIndex,
|
||||
destinations: widget.destinations,
|
||||
currentLocation: currentLocation,
|
||||
drawerKey: _key,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue