Add linux deb dependencies Add backup recovery strategy select Support custom text scaling Optimize the display of different text scale Optimize windows setup experience Optimize startTun performance Optimize android tv experience Optimize default option Optimize computed text size Optimize hyperOS freeform window Add developer mode Update core Optimize more details
339 lines
10 KiB
Dart
339 lines
10 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:fl_clash/common/common.dart';
|
|
import 'package:fl_clash/enum/enum.dart';
|
|
import 'package:fl_clash/models/models.dart';
|
|
import 'package:fl_clash/providers/providers.dart';
|
|
import 'package:fl_clash/state.dart';
|
|
import 'package:fl_clash/widgets/widgets.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
typedef OnSelected = void Function(int index);
|
|
|
|
class HomePage extends StatelessWidget {
|
|
const HomePage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return HomeBackScope(
|
|
child: Consumer(
|
|
builder: (_, ref, child) {
|
|
final state = ref.watch(homeStateProvider);
|
|
final viewMode = state.viewMode;
|
|
final navigationItems = state.navigationItems;
|
|
final pageLabel = state.pageLabel;
|
|
final index = navigationItems.lastIndexWhere(
|
|
(element) => element.label == pageLabel,
|
|
);
|
|
final currentIndex = index == -1 ? 0 : index;
|
|
final navigationBar = CommonNavigationBar(
|
|
viewMode: viewMode,
|
|
navigationItems: navigationItems,
|
|
currentIndex: currentIndex,
|
|
);
|
|
final bottomNavigationBar =
|
|
viewMode == ViewMode.mobile ? navigationBar : null;
|
|
final sideNavigationBar =
|
|
viewMode != ViewMode.mobile ? navigationBar : null;
|
|
return CommonScaffold(
|
|
key: globalState.homeScaffoldKey,
|
|
title: Intl.message(
|
|
pageLabel.name,
|
|
),
|
|
sideNavigationBar: sideNavigationBar,
|
|
body: child!,
|
|
bottomNavigationBar: bottomNavigationBar,
|
|
);
|
|
},
|
|
child: _HomePageView(),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _HomePageView extends ConsumerStatefulWidget {
|
|
const _HomePageView();
|
|
|
|
@override
|
|
ConsumerState createState() => _HomePageViewState();
|
|
}
|
|
|
|
class _HomePageViewState extends ConsumerState<_HomePageView> {
|
|
late PageController _pageController;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_pageController = PageController(
|
|
initialPage: _pageIndex,
|
|
keepPage: true,
|
|
);
|
|
ref.listenManual(currentPageLabelProvider, (prev, next) {
|
|
if (prev != next) {
|
|
_toPage(next);
|
|
}
|
|
});
|
|
ref.listenManual(currentNavigationsStateProvider, (prev, next) {
|
|
if (prev?.value.length != next.value.length) {
|
|
_updatePageController();
|
|
}
|
|
});
|
|
}
|
|
|
|
int get _pageIndex {
|
|
final navigationItems = ref.read(currentNavigationsStateProvider).value;
|
|
return navigationItems.indexWhere(
|
|
(item) => item.label == globalState.appState.pageLabel,
|
|
);
|
|
}
|
|
|
|
_toPage(PageLabel pageLabel, [bool ignoreAnimateTo = false]) async {
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
final navigationItems = ref.read(currentNavigationsStateProvider).value;
|
|
final index = navigationItems.indexWhere((item) => item.label == pageLabel);
|
|
if (index == -1) {
|
|
return;
|
|
}
|
|
final isAnimateToPage = ref.read(appSettingProvider).isAnimateToPage;
|
|
final isMobile = ref.read(isMobileViewProvider);
|
|
if (isAnimateToPage && isMobile && !ignoreAnimateTo) {
|
|
await _pageController.animateToPage(
|
|
index,
|
|
duration: kTabScrollDuration,
|
|
curve: Curves.easeOut,
|
|
);
|
|
} else {
|
|
_pageController.jumpToPage(index);
|
|
}
|
|
}
|
|
|
|
_updatePageController() {
|
|
final pageLabel = globalState.appState.pageLabel;
|
|
_toPage(pageLabel, true);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_pageController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final navigationItems = ref.watch(currentNavigationsStateProvider).value;
|
|
return PageView.builder(
|
|
controller: _pageController,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: navigationItems.length,
|
|
// onPageChanged: (index) {
|
|
// debouncer.call(DebounceTag.pageChange, () {
|
|
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
// if (_pageIndex != index) {
|
|
// final pageLabel = navigationItems[index].label;
|
|
// _toPage(pageLabel, true);
|
|
// }
|
|
// });
|
|
// });
|
|
// },
|
|
itemBuilder: (_, index) {
|
|
final navigationItem = navigationItems[index];
|
|
return KeepScope(
|
|
keep: navigationItem.keep,
|
|
key: Key(navigationItem.label.name),
|
|
child: navigationItem.fragment,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class CommonNavigationBar extends ConsumerWidget {
|
|
final ViewMode viewMode;
|
|
final List<NavigationItem> navigationItems;
|
|
final int currentIndex;
|
|
|
|
const CommonNavigationBar({
|
|
super.key,
|
|
required this.viewMode,
|
|
required this.navigationItems,
|
|
required this.currentIndex,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
if (viewMode == ViewMode.mobile) {
|
|
return NavigationBarTheme(
|
|
data: _NavigationBarDefaultsM3(context),
|
|
child: NavigationBar(
|
|
destinations: navigationItems
|
|
.map(
|
|
(e) => NavigationDestination(
|
|
icon: e.icon,
|
|
label: Intl.message(e.label.name),
|
|
),
|
|
)
|
|
.toList(),
|
|
onDestinationSelected: (index) {
|
|
globalState.appController.toPage(navigationItems[index].label);
|
|
},
|
|
selectedIndex: currentIndex,
|
|
),
|
|
);
|
|
}
|
|
final showLabel = ref.watch(appSettingProvider).showLabel;
|
|
return Material(
|
|
color: context.colorScheme.surfaceContainer,
|
|
child: Column(
|
|
children: [
|
|
Expanded(
|
|
child: ScrollConfiguration(
|
|
behavior: HiddenBarScrollBehavior(),
|
|
child: SingleChildScrollView(
|
|
child: IntrinsicHeight(
|
|
child: NavigationRail(
|
|
backgroundColor: context.colorScheme.surfaceContainer,
|
|
selectedIconTheme: IconThemeData(
|
|
color: context.colorScheme.onSurfaceVariant,
|
|
),
|
|
unselectedIconTheme: IconThemeData(
|
|
color: context.colorScheme.onSurfaceVariant,
|
|
),
|
|
selectedLabelTextStyle:
|
|
context.textTheme.labelLarge!.copyWith(
|
|
color: context.colorScheme.onSurface,
|
|
),
|
|
unselectedLabelTextStyle:
|
|
context.textTheme.labelLarge!.copyWith(
|
|
color: context.colorScheme.onSurface,
|
|
),
|
|
destinations: navigationItems
|
|
.map(
|
|
(e) => NavigationRailDestination(
|
|
icon: e.icon,
|
|
label: Text(
|
|
Intl.message(e.label.name),
|
|
),
|
|
),
|
|
)
|
|
.toList(),
|
|
onDestinationSelected: (index) {
|
|
globalState.appController
|
|
.toPage(navigationItems[index].label);
|
|
},
|
|
extended: false,
|
|
selectedIndex: currentIndex,
|
|
labelType: showLabel
|
|
? NavigationRailLabelType.all
|
|
: NavigationRailLabelType.none,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(
|
|
height: 16,
|
|
),
|
|
IconButton(
|
|
onPressed: () {
|
|
ref.read(appSettingProvider.notifier).updateState(
|
|
(state) => state.copyWith(
|
|
showLabel: !state.showLabel,
|
|
),
|
|
);
|
|
},
|
|
icon: const Icon(Icons.menu),
|
|
),
|
|
const SizedBox(
|
|
height: 16,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _NavigationBarDefaultsM3 extends NavigationBarThemeData {
|
|
_NavigationBarDefaultsM3(this.context)
|
|
: super(
|
|
height: 80.0,
|
|
elevation: 3.0,
|
|
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
|
|
);
|
|
|
|
final BuildContext context;
|
|
late final ColorScheme _colors = Theme.of(context).colorScheme;
|
|
late final TextTheme _textTheme = Theme.of(context).textTheme;
|
|
|
|
@override
|
|
Color? get backgroundColor => _colors.surfaceContainer;
|
|
|
|
@override
|
|
Color? get shadowColor => Colors.transparent;
|
|
|
|
@override
|
|
Color? get surfaceTintColor => Colors.transparent;
|
|
|
|
@override
|
|
WidgetStateProperty<IconThemeData?>? get iconTheme {
|
|
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
|
return IconThemeData(
|
|
size: 24.0,
|
|
color: states.contains(WidgetState.disabled)
|
|
? _colors.onSurfaceVariant.opacity38
|
|
: states.contains(WidgetState.selected)
|
|
? _colors.onSecondaryContainer
|
|
: _colors.onSurfaceVariant,
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Color? get indicatorColor => _colors.secondaryContainer;
|
|
|
|
@override
|
|
ShapeBorder? get indicatorShape => const StadiumBorder();
|
|
|
|
@override
|
|
WidgetStateProperty<TextStyle?>? get labelTextStyle {
|
|
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
|
final TextStyle style = _textTheme.labelMedium!;
|
|
return style.apply(
|
|
overflow: TextOverflow.ellipsis,
|
|
color: states.contains(WidgetState.disabled)
|
|
? _colors.onSurfaceVariant.opacity38
|
|
: states.contains(WidgetState.selected)
|
|
? _colors.onSurface
|
|
: _colors.onSurfaceVariant);
|
|
});
|
|
}
|
|
}
|
|
|
|
class HomeBackScope extends StatelessWidget {
|
|
final Widget child;
|
|
|
|
const HomeBackScope({super.key, required this.child});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (Platform.isAndroid) {
|
|
return CommonPopScope(
|
|
onPop: () async {
|
|
final canPop = Navigator.canPop(context);
|
|
if (canPop) {
|
|
Navigator.pop(context);
|
|
} else {
|
|
await globalState.appController.handleBackOrExit();
|
|
}
|
|
return false;
|
|
},
|
|
child: child,
|
|
);
|
|
}
|
|
return child;
|
|
}
|
|
}
|