Release Lunaris 2.0 customization update
This commit is contained in:
@@ -1,33 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../app/app.dart';
|
||||
import '../../app/motion.dart';
|
||||
import '../../app/responsive.dart';
|
||||
import '../../backend/backend.dart';
|
||||
import '../../core/utils/result.dart';
|
||||
|
||||
class AppShell extends StatelessWidget {
|
||||
class AppShell extends ConsumerWidget {
|
||||
const AppShell({required this.child, super.key});
|
||||
|
||||
final Widget child;
|
||||
|
||||
static const destinations = [
|
||||
_Destination('Feed', Icons.dashboard_rounded, '/'),
|
||||
_Destination('Search', Icons.search_rounded, '/search'),
|
||||
_Destination('Favorites', Icons.favorite_rounded, '/favorites'),
|
||||
_Destination('Viewed', Icons.history_rounded, '/viewed'),
|
||||
_Destination('feed', 'Feed', Icons.dashboard_rounded, '/'),
|
||||
_Destination('search', 'Search', Icons.search_rounded, '/search'),
|
||||
_Destination(
|
||||
'favorites', 'Favorites', Icons.favorite_rounded, '/favorites'),
|
||||
_Destination('viewed', 'Viewed', Icons.history_rounded, '/viewed'),
|
||||
_Destination(
|
||||
'collections',
|
||||
'Collections',
|
||||
Icons.collections_bookmark_rounded,
|
||||
'/collections',
|
||||
mobileLabel: 'Boards',
|
||||
),
|
||||
_Destination('Artists', Icons.person_search_rounded, '/artists'),
|
||||
_Destination('Providers', Icons.hub_rounded, '/providers'),
|
||||
_Destination('Settings', Icons.settings_rounded, '/settings'),
|
||||
_Destination('artists', 'Artists', Icons.person_search_rounded, '/artists'),
|
||||
_Destination('providers', 'Providers', Icons.hub_rounded, '/providers'),
|
||||
_Destination('settings', 'Settings', Icons.settings_rounded, '/settings'),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final settings =
|
||||
ref.watch(appSettingsProvider).value ?? AppSettings.defaults;
|
||||
final artistConfigs =
|
||||
ref.watch(_enabledArtistConfigsProvider).value ?? const [];
|
||||
final destinations =
|
||||
_visibleDestinations(settings, artistConfigs.isNotEmpty);
|
||||
final ru = settings.languageCode == 'ru';
|
||||
return Shortcuts(
|
||||
shortcuts: {
|
||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyF):
|
||||
@@ -67,17 +80,42 @@ class AppShell extends StatelessWidget {
|
||||
child: Focus(
|
||||
autofocus: true,
|
||||
child: Responsive.isDesktop(context)
|
||||
? _DesktopShell(child: child)
|
||||
: _MobileShell(child: child),
|
||||
? _DesktopShell(
|
||||
destinations: destinations,
|
||||
ru: ru,
|
||||
child: child,
|
||||
)
|
||||
: _MobileShell(
|
||||
destinations: destinations,
|
||||
ru: ru,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static List<_Destination> _visibleDestinations(
|
||||
AppSettings settings,
|
||||
bool hasArtists,
|
||||
) {
|
||||
return destinations.where((item) {
|
||||
if (settings.hiddenTabs.contains(item.id)) return false;
|
||||
if (item.id == 'artists' && !hasArtists) return false;
|
||||
return true;
|
||||
}).toList(growable: false);
|
||||
}
|
||||
}
|
||||
|
||||
class _DesktopShell extends StatefulWidget {
|
||||
const _DesktopShell({required this.child});
|
||||
const _DesktopShell({
|
||||
required this.child,
|
||||
required this.destinations,
|
||||
required this.ru,
|
||||
});
|
||||
final Widget child;
|
||||
final List<_Destination> destinations;
|
||||
final bool ru;
|
||||
|
||||
@override
|
||||
State<_DesktopShell> createState() => _DesktopShellState();
|
||||
@@ -89,7 +127,7 @@ class _DesktopShellState extends State<_DesktopShell> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final location = GoRouterState.of(context).uri.path;
|
||||
final selected = AppShell.destinations.indexWhere(
|
||||
final selected = widget.destinations.indexWhere(
|
||||
(item) => item.location == '/'
|
||||
? location == '/'
|
||||
: location.startsWith(item.location),
|
||||
@@ -112,14 +150,15 @@ class _DesktopShellState extends State<_DesktopShell> {
|
||||
child: Column(
|
||||
children: [
|
||||
for (var index = 0;
|
||||
index < AppShell.destinations.length;
|
||||
index < widget.destinations.length;
|
||||
index++)
|
||||
_RailButton(
|
||||
destination: AppShell.destinations[index],
|
||||
destination: widget.destinations[index],
|
||||
ru: widget.ru,
|
||||
selected: (selected < 0 ? 0 : selected) == index,
|
||||
expanded: _hovered,
|
||||
onTap: () {
|
||||
context.go(AppShell.destinations[index].location);
|
||||
context.go(widget.destinations[index].location);
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
@@ -142,12 +181,14 @@ class _RailButton extends StatelessWidget {
|
||||
required this.destination,
|
||||
required this.selected,
|
||||
required this.expanded,
|
||||
required this.ru,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final _Destination destination;
|
||||
final bool selected;
|
||||
final bool expanded;
|
||||
final bool ru;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
@@ -156,7 +197,7 @@ class _RailButton extends StatelessWidget {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Tooltip(
|
||||
message: expanded ? '' : destination.label,
|
||||
message: expanded ? '' : destination.labelFor(ru),
|
||||
waitDuration: const Duration(milliseconds: 450),
|
||||
child: Material(
|
||||
color: selected ? colors.primaryContainer : Colors.transparent,
|
||||
@@ -187,7 +228,7 @@ class _RailButton extends StatelessWidget {
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
destination.label,
|
||||
destination.labelFor(ru),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.clip,
|
||||
softWrap: false,
|
||||
@@ -218,12 +259,18 @@ class _RailButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _MobileShell extends StatelessWidget {
|
||||
const _MobileShell({required this.child});
|
||||
const _MobileShell({
|
||||
required this.child,
|
||||
required this.destinations,
|
||||
required this.ru,
|
||||
});
|
||||
final Widget child;
|
||||
final List<_Destination> destinations;
|
||||
final bool ru;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = AppShell.destinations
|
||||
final items = destinations
|
||||
.where((item) =>
|
||||
item.location != '/providers' && item.location != '/settings')
|
||||
.take(6)
|
||||
@@ -240,7 +287,7 @@ class _MobileShell extends StatelessWidget {
|
||||
? null
|
||||
: FloatingActionButton.small(
|
||||
heroTag: 'mobile-settings',
|
||||
tooltip: 'Settings',
|
||||
tooltip: ru ? 'Настройки' : 'Settings',
|
||||
onPressed: () => context.go('/settings'),
|
||||
child: const Icon(Icons.settings_rounded),
|
||||
),
|
||||
@@ -252,7 +299,7 @@ class _MobileShell extends StatelessWidget {
|
||||
for (final item in items)
|
||||
NavigationDestination(
|
||||
icon: Icon(item.icon),
|
||||
label: item.mobileLabel,
|
||||
label: item.mobileLabelFor(ru),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -262,18 +309,54 @@ class _MobileShell extends StatelessWidget {
|
||||
|
||||
class _Destination {
|
||||
const _Destination(
|
||||
this.id,
|
||||
this.label,
|
||||
this.icon,
|
||||
this.location, {
|
||||
String? mobileLabel,
|
||||
}) : mobileLabel = mobileLabel ?? label;
|
||||
|
||||
final String id;
|
||||
final String label;
|
||||
final String mobileLabel;
|
||||
final IconData icon;
|
||||
final String location;
|
||||
|
||||
String labelFor(bool ru) {
|
||||
if (!ru) return label;
|
||||
return switch (id) {
|
||||
'feed' => 'Лента',
|
||||
'search' => 'Поиск',
|
||||
'favorites' => 'Избранное',
|
||||
'viewed' => 'История',
|
||||
'collections' => 'Коллекции',
|
||||
'artists' => 'Авторы',
|
||||
'providers' => 'Провайдеры',
|
||||
'settings' => 'Настройки',
|
||||
_ => label,
|
||||
};
|
||||
}
|
||||
|
||||
String mobileLabelFor(bool ru) {
|
||||
if (!ru) return mobileLabel;
|
||||
return switch (id) {
|
||||
'collections' => 'Колл.',
|
||||
_ => labelFor(true),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
final _enabledArtistConfigsProvider =
|
||||
FutureProvider<List<ContentProviderConfig>>((ref) async {
|
||||
ref.watch(appSettingsProvider);
|
||||
final result = await ref
|
||||
.watch(providerManagerProvider)
|
||||
.loadArtistConfigs(enabledOnly: true);
|
||||
return result is Success<List<ContentProviderConfig>>
|
||||
? result.data
|
||||
: const [];
|
||||
});
|
||||
|
||||
class _NavigateIntent extends Intent {
|
||||
const _NavigateIntent(this.location);
|
||||
final String location;
|
||||
|
||||
Reference in New Issue
Block a user