refactor(mobile): pages (#9246)
* refactor(mobile): pages * refactor * album pages * pages * pages * use *.page.dart * representation * put back module
This commit is contained in:
@@ -25,7 +25,7 @@ import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||
import 'package:immich_mobile/utils/selection_handlers.dart';
|
||||
|
||||
class MultiselectGrid extends HookConsumerWidget {
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AppLogDetailPage extends HookConsumerWidget {
|
||||
const AppLogDetailPage({super.key, required this.logMessage});
|
||||
|
||||
final LoggerMessage logMessage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
var isDarkTheme = context.isDarkTheme;
|
||||
|
||||
buildTextWithCopyButton(String header, String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
header,
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: context.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: text)).then((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"Copied to clipboard",
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.copy,
|
||||
size: 16.0,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isDarkTheme ? Colors.grey[900] : Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SelectableText(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: "Inconsolata",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildLogContext1(String context1) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
"FROM",
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: context.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isDarkTheme ? Colors.grey[900] : Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SelectableText(
|
||||
context1.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: "Inconsolata",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Log Detail"),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
children: [
|
||||
buildTextWithCopyButton("MESSAGE", logMessage.message),
|
||||
if (logMessage.details != null)
|
||||
buildTextWithCopyButton("DETAILS", logMessage.details.toString()),
|
||||
if (logMessage.context1 != null)
|
||||
buildLogContext1(logMessage.context1.toString()),
|
||||
if (logMessage.context2 != null)
|
||||
buildTextWithCopyButton(
|
||||
"STACK TRACE",
|
||||
logMessage.context2.toString(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
||||
import 'package:immich_mobile/services/immich_logger.service.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AppLogPage extends HookConsumerWidget {
|
||||
const AppLogPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final immichLogger = ImmichLogger();
|
||||
final logMessages = useState(immichLogger.messages);
|
||||
final isDarkTheme = context.isDarkTheme;
|
||||
|
||||
Widget colorStatusIndicator(Color color) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildLeadingIcon(LogLevel level) {
|
||||
switch (level) {
|
||||
case LogLevel.INFO:
|
||||
return colorStatusIndicator(context.primaryColor);
|
||||
case LogLevel.SEVERE:
|
||||
return colorStatusIndicator(Colors.redAccent);
|
||||
|
||||
case LogLevel.WARNING:
|
||||
return colorStatusIndicator(Colors.orangeAccent);
|
||||
default:
|
||||
return colorStatusIndicator(Colors.grey);
|
||||
}
|
||||
}
|
||||
|
||||
getTileColor(LogLevel level) {
|
||||
switch (level) {
|
||||
case LogLevel.INFO:
|
||||
return Colors.transparent;
|
||||
case LogLevel.SEVERE:
|
||||
return isDarkTheme
|
||||
? Colors.redAccent.withOpacity(0.25)
|
||||
: Colors.redAccent.withOpacity(0.075);
|
||||
case LogLevel.WARNING:
|
||||
return isDarkTheme
|
||||
? Colors.orangeAccent.withOpacity(0.25)
|
||||
: Colors.orangeAccent.withOpacity(0.075);
|
||||
default:
|
||||
return context.primaryColor.withOpacity(0.1);
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
"Logs",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
scrolledUnderElevation: 1,
|
||||
elevation: 2,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.delete_outline_rounded,
|
||||
color: context.primaryColor,
|
||||
semanticLabel: "Clear logs",
|
||||
size: 20.0,
|
||||
),
|
||||
onPressed: () {
|
||||
immichLogger.clearLogs();
|
||||
logMessages.value = [];
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.share_rounded,
|
||||
color: context.primaryColor,
|
||||
semanticLabel: "Share logs",
|
||||
size: 20.0,
|
||||
),
|
||||
onPressed: () {
|
||||
immichLogger.shareLogs();
|
||||
},
|
||||
),
|
||||
],
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
context.popRoute();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
size: 20.0,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: ListView.separated(
|
||||
separatorBuilder: (context, index) {
|
||||
return Divider(
|
||||
height: 0,
|
||||
color: isDarkTheme ? Colors.white70 : Colors.grey[600],
|
||||
);
|
||||
},
|
||||
itemCount: logMessages.value.length,
|
||||
itemBuilder: (context, index) {
|
||||
var logMessage = logMessages.value[index];
|
||||
return ListTile(
|
||||
onTap: () => context.pushRoute(
|
||||
AppLogDetailRoute(
|
||||
logMessage: logMessage,
|
||||
),
|
||||
),
|
||||
trailing: const Icon(Icons.arrow_forward_ios_rounded),
|
||||
visualDensity: VisualDensity.compact,
|
||||
dense: true,
|
||||
tileColor: getTileColor(logMessage.level),
|
||||
minLeadingWidth: 10,
|
||||
title: Text(
|
||||
truncateLogMessage(logMessage.message, 4),
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontFamily: "Inconsolata",
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
"at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.context1}",
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
leading: buildLeadingIcon(logMessage.level),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Truncate the log message to a certain number of lines
|
||||
/// @param int maxLines - Max number of lines to truncate
|
||||
String truncateLogMessage(String message, int maxLines) {
|
||||
List<String> messageLines = message.split("\n");
|
||||
if (messageLines.length < maxLines) {
|
||||
return message;
|
||||
}
|
||||
return "${messageLines.sublist(0, maxLines).join("\n")} ...";
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart';
|
||||
|
||||
final _loadingEntry = OverlayEntry(
|
||||
builder: (context) => SizedBox.square(
|
||||
dimension: double.infinity,
|
||||
child: DecoratedBox(
|
||||
decoration:
|
||||
BoxDecoration(color: context.colorScheme.surface.withAlpha(200)),
|
||||
child: const Center(
|
||||
child: DelayedLoadingIndicator(
|
||||
delay: Duration(seconds: 1),
|
||||
fadeInDuration: Duration(milliseconds: 400),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
ValueNotifier<bool> useProcessingOverlay() {
|
||||
return use(const _LoadingOverlay());
|
||||
}
|
||||
|
||||
class _LoadingOverlay extends Hook<ValueNotifier<bool>> {
|
||||
const _LoadingOverlay();
|
||||
|
||||
@override
|
||||
_LoadingOverlayState createState() => _LoadingOverlayState();
|
||||
}
|
||||
|
||||
class _LoadingOverlayState
|
||||
extends HookState<ValueNotifier<bool>, _LoadingOverlay> {
|
||||
late final _isLoading = ValueNotifier(false)..addListener(_listener);
|
||||
OverlayEntry? _loadingOverlay;
|
||||
|
||||
void _listener() {
|
||||
setState(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_isLoading.value) {
|
||||
_loadingOverlay?.remove();
|
||||
_loadingOverlay = _loadingEntry;
|
||||
Overlay.of(context).insert(_loadingEntry);
|
||||
} else {
|
||||
_loadingOverlay?.remove();
|
||||
_loadingOverlay = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
ValueNotifier<bool> build(BuildContext context) {
|
||||
return _isLoading;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_isLoading.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Object? get debugValue => _isLoading.value;
|
||||
|
||||
@override
|
||||
String get debugLabel => 'useProcessingOverlay<>';
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
@RoutePage()
|
||||
class SplashScreenPage extends HookConsumerWidget {
|
||||
const SplashScreenPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final apiService = ref.watch(apiServiceProvider);
|
||||
final serverUrl = Store.tryGet(StoreKey.serverUrl);
|
||||
final accessToken = Store.tryGet(StoreKey.accessToken);
|
||||
final log = Logger("SplashScreenPage");
|
||||
|
||||
void performLoggingIn() async {
|
||||
bool isSuccess = false;
|
||||
bool deviceIsOffline = false;
|
||||
|
||||
if (accessToken != null && serverUrl != null) {
|
||||
try {
|
||||
// Resolve API server endpoint from user provided serverUrl
|
||||
await apiService.resolveAndSetEndpoint(serverUrl);
|
||||
} on ApiException catch (error, stackTrace) {
|
||||
log.severe(
|
||||
"Failed to resolve endpoint [ApiException]",
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
// okay, try to continue anyway if offline
|
||||
if (error.code == 503) {
|
||||
deviceIsOffline = true;
|
||||
log.warning("Device seems to be offline upon launch");
|
||||
} else {
|
||||
log.severe("Failed to resolve endpoint", error);
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
log.severe(
|
||||
"Failed to resolve endpoint [Catch All]",
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
isSuccess = await ref
|
||||
.read(authenticationProvider.notifier)
|
||||
.setSuccessLoginInfo(
|
||||
accessToken: accessToken,
|
||||
serverUrl: serverUrl,
|
||||
offlineLogin: deviceIsOffline,
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
log.severe(
|
||||
'Cannot set success login info',
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If the device is offline and there is a currentUser stored locallly
|
||||
// Proceed into the app
|
||||
if (deviceIsOffline && Store.tryGet(StoreKey.currentUser) != null) {
|
||||
context.replaceRoute(const TabControllerRoute());
|
||||
} else if (isSuccess) {
|
||||
// If device was able to login through the internet successfully
|
||||
final hasPermission =
|
||||
await ref.read(galleryPermissionNotifier.notifier).hasPermission;
|
||||
if (hasPermission) {
|
||||
// Resume backup (if enable) then navigate
|
||||
ref.watch(backupProvider.notifier).resumeBackup();
|
||||
}
|
||||
context.replaceRoute(const TabControllerRoute());
|
||||
} else {
|
||||
log.severe(
|
||||
'Unable to login through offline or online methods - logging out completely',
|
||||
);
|
||||
|
||||
ref.read(authenticationProvider.notifier).logout();
|
||||
// User was unable to login through either offline or online methods
|
||||
context.replaceRoute(const LoginRoute());
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
if (serverUrl != null && accessToken != null) {
|
||||
performLoggingIn();
|
||||
} else {
|
||||
context.replaceRoute(const LoginRoute());
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Image(
|
||||
image: AssetImage('assets/immich-logo.png'),
|
||||
width: 80,
|
||||
filterQuality: FilterQuality.high,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||
|
||||
@RoutePage()
|
||||
class TabControllerPage extends HookConsumerWidget {
|
||||
const TabControllerPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final refreshing = ref.watch(assetProvider);
|
||||
|
||||
Widget buildIcon(Widget icon) {
|
||||
if (!refreshing) return icon;
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
icon,
|
||||
Positioned(
|
||||
right: -14,
|
||||
child: SizedBox(
|
||||
height: 12,
|
||||
width: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
navigationRail(TabsRouter tabsRouter) {
|
||||
return NavigationRail(
|
||||
labelType: NavigationRailLabelType.all,
|
||||
selectedIndex: tabsRouter.activeIndex,
|
||||
onDestinationSelected: (index) {
|
||||
// Selected Photos while it is active
|
||||
if (tabsRouter.activeIndex == 0 && index == 0) {
|
||||
// Scroll to top
|
||||
scrollToTopNotifierProvider.scrollToTop();
|
||||
}
|
||||
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
tabsRouter.setActiveIndex(index);
|
||||
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
||||
},
|
||||
selectedIconTheme: IconThemeData(
|
||||
color: context.primaryColor,
|
||||
),
|
||||
selectedLabelTextStyle: TextStyle(
|
||||
color: context.primaryColor,
|
||||
),
|
||||
useIndicator: false,
|
||||
destinations: [
|
||||
NavigationRailDestination(
|
||||
padding: EdgeInsets.only(
|
||||
top: MediaQuery.of(context).padding.top + 4,
|
||||
left: 4,
|
||||
right: 4,
|
||||
bottom: 4,
|
||||
),
|
||||
icon: const Icon(Icons.photo_library_outlined),
|
||||
selectedIcon: const Icon(Icons.photo_library),
|
||||
label: const Text('tab_controller_nav_photos').tr(),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
padding: const EdgeInsets.all(4),
|
||||
icon: const Icon(Icons.search_rounded),
|
||||
selectedIcon: const Icon(Icons.search),
|
||||
label: const Text('tab_controller_nav_search').tr(),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
padding: const EdgeInsets.all(4),
|
||||
icon: const Icon(Icons.share_rounded),
|
||||
selectedIcon: const Icon(Icons.share),
|
||||
label: const Text('tab_controller_nav_sharing').tr(),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
padding: const EdgeInsets.all(4),
|
||||
icon: const Icon(Icons.photo_album_outlined),
|
||||
selectedIcon: const Icon(Icons.photo_album),
|
||||
label: const Text('tab_controller_nav_library').tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
bottomNavigationBar(TabsRouter tabsRouter) {
|
||||
return NavigationBar(
|
||||
selectedIndex: tabsRouter.activeIndex,
|
||||
onDestinationSelected: (index) {
|
||||
if (tabsRouter.activeIndex == 0 && index == 0) {
|
||||
// Scroll to top
|
||||
scrollToTopNotifierProvider.scrollToTop();
|
||||
}
|
||||
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
tabsRouter.setActiveIndex(index);
|
||||
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
||||
},
|
||||
destinations: [
|
||||
NavigationDestination(
|
||||
label: 'tab_controller_nav_photos'.tr(),
|
||||
icon: const Icon(
|
||||
Icons.photo_library_outlined,
|
||||
),
|
||||
selectedIcon: buildIcon(
|
||||
Icon(
|
||||
Icons.photo_library,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'tab_controller_nav_search'.tr(),
|
||||
icon: const Icon(
|
||||
Icons.search_rounded,
|
||||
),
|
||||
selectedIcon: Icon(
|
||||
Icons.search,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'tab_controller_nav_sharing'.tr(),
|
||||
icon: const Icon(
|
||||
Icons.group_outlined,
|
||||
),
|
||||
selectedIcon: Icon(
|
||||
Icons.group,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'tab_controller_nav_library'.tr(),
|
||||
icon: const Icon(
|
||||
Icons.photo_album_outlined,
|
||||
),
|
||||
selectedIcon: buildIcon(
|
||||
Icon(
|
||||
Icons.photo_album_rounded,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final multiselectEnabled = ref.watch(multiselectProvider);
|
||||
return AutoTabsRouter(
|
||||
routes: const [
|
||||
HomeRoute(),
|
||||
SearchRoute(),
|
||||
SharingRoute(),
|
||||
LibraryRoute(),
|
||||
],
|
||||
duration: const Duration(milliseconds: 600),
|
||||
transitionBuilder: (context, child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
builder: (context, child) {
|
||||
final tabsRouter = AutoTabsRouter.of(context);
|
||||
return PopScope(
|
||||
canPop: tabsRouter.activeIndex == 0,
|
||||
onPopInvoked: (didPop) =>
|
||||
!didPop ? tabsRouter.setActiveIndex(0) : null,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
const medium = 600;
|
||||
final Widget? bottom;
|
||||
final Widget body;
|
||||
if (constraints.maxWidth < medium) {
|
||||
// Normal phone width
|
||||
bottom = bottomNavigationBar(tabsRouter);
|
||||
body = child;
|
||||
} else {
|
||||
// Medium tablet width
|
||||
bottom = null;
|
||||
body = Row(
|
||||
children: [
|
||||
navigationRail(tabsRouter),
|
||||
Expanded(child: child),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
body: HeroControllerScope(
|
||||
controller: HeroController(),
|
||||
child: body,
|
||||
),
|
||||
bottomNavigationBar: multiselectEnabled ? null : bottom,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user