new: custom tabs (#889)

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-09-03 01:05:03 +08:00
committed by GitHub
parent 2466341999
commit e51804fa70
36 changed files with 601 additions and 67 deletions

View File

@@ -33,6 +33,7 @@ class _HomePageState extends ConsumerState<HomePage>
late final _notifier = ref.read(serversNotifierProvider.notifier);
late final _provider = ref.read(serversNotifierProvider);
late List<AppTab> _tabs = Stores.setting.homeTabs.fetch();
@override
void dispose() {
@@ -51,13 +52,30 @@ class _HomePageState extends ConsumerState<HomePage>
SystemUIs.switchStatusBar(hide: false);
WidgetsBinding.instance.addObserver(this);
// avoid index out of range
if (_selectIndex.value >= AppTab.values.length || _selectIndex.value < 0) {
if (_selectIndex.value >= _tabs.length || _selectIndex.value < 0) {
_selectIndex.value = 0;
}
_pageController = PageController(initialPage: _selectIndex.value);
if (Stores.setting.generalWakeLock.fetch()) {
WakelockPlus.enable();
}
// Listen to homeTabs changes
Stores.setting.homeTabs.listenable().addListener(() {
final newTabs = Stores.setting.homeTabs.fetch();
if (mounted && newTabs != _tabs) {
setState(() {
_tabs = newTabs;
// Ensure current page index is valid
if (_selectIndex.value >= _tabs.length) {
_selectIndex.value = _tabs.length - 1;
}
if (_selectIndex.value < 0 && _tabs.isNotEmpty) {
_selectIndex.value = 0;
}
});
}
});
}
@override
@@ -119,9 +137,9 @@ class _HomePageState extends ConsumerState<HomePage>
Expanded(
child: PageView.builder(
controller: _pageController,
itemCount: AppTab.values.length,
itemCount: _tabs.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (_, index) => AppTab.values[index].page,
itemBuilder: (_, index) => _tabs[index].page,
onPageChanged: (value) {
FocusScope.of(context).unfocus();
if (!_switchingPage) {
@@ -146,7 +164,7 @@ class _HomePageState extends ConsumerState<HomePage>
animationDuration: const Duration(milliseconds: 250),
onDestinationSelected: _onDestinationSelected,
labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected,
destinations: AppTab.navDestinations,
destinations: _tabs.map((tab) => tab.navDestination).toList(),
),
);
}
@@ -165,7 +183,7 @@ class _HomePageState extends ConsumerState<HomePage>
trailing: extended ? const SizedBox(height: 20) : null,
labelType: extended ? NavigationRailLabelType.none : NavigationRailLabelType.all,
selectedIndex: idx,
destinations: AppTab.navRailDestinations,
destinations: _tabs.map((tab) => tab.navRailDestination).toList(),
onDestinationSelected: _onDestinationSelected,
),
),
@@ -236,6 +254,7 @@ class _HomePageState extends ConsumerState<HomePage>
void _onDestinationSelected(int index) {
if (_selectIndex.value == index) return;
if (index < 0 || index >= _tabs.length) return;
_selectIndex.value = index;
_switchingPage = true;
_pageController.animateToPage(

View File

@@ -8,6 +8,7 @@ extension _App on _AppSettingsPageState {
_buildThemeMode(),
_buildAppColor(),
_buildCheckUpdate(),
_buildHomeTabs(),
PlatformPublicSettings.buildBioAuth,
if (specific != null) specific,
_buildAppMore(),
@@ -274,4 +275,15 @@ extension _App on _AppSettingsPageState {
trailing: StoreSwitch(prop: _setting.hideTitleBar),
);
}
Widget _buildHomeTabs() {
return ListTile(
leading: const Icon(Icons.tab),
title: Text(l10n.homeTabs),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () {
HomeTabsConfigPage.route.go(context);
},
);
}
}

View File

@@ -0,0 +1,130 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/app/tab.dart';
import 'package:server_box/data/res/store.dart';
class HomeTabsConfigPage extends ConsumerStatefulWidget {
const HomeTabsConfigPage({super.key});
static final route = AppRouteNoArg(page: HomeTabsConfigPage.new, path: '/settings/home-tabs');
@override
ConsumerState<HomeTabsConfigPage> createState() => _HomeTabsConfigPageState();
}
class _HomeTabsConfigPageState extends ConsumerState<HomeTabsConfigPage> {
final _availableTabs = AppTab.values;
var _selectedTabs = List<AppTab>.from(Stores.setting.homeTabs.fetch());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(
title: Text(l10n.homeTabs),
actions: [
TextButton(onPressed: _resetToDefault, child: Text(libL10n.reset)),
TextButton(onPressed: _saveAndExit, child: Text(libL10n.save)),
],
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(l10n.homeTabsCustomizeDesc, style: context.theme.textTheme.bodyMedium),
),
Expanded(
child: ReorderableListView.builder(
itemCount: _selectedTabs.length,
onReorder: _onReorder,
buildDefaultDragHandles: false,
itemBuilder: (context, index) {
final tab = _selectedTabs[index];
return _buildTabItem(tab, index, true);
},
),
),
const Divider(),
Padding(
padding: const EdgeInsets.all(16),
child: Text(l10n.availableTabs, style: context.theme.textTheme.titleMedium),
),
Expanded(
child: ListView.builder(
itemCount: _availableTabs.length,
itemBuilder: (context, index) {
final tab = _availableTabs[index];
if (_selectedTabs.contains(tab)) {
return const SizedBox.shrink();
}
return _buildTabItem(tab, index, false);
},
),
),
],
),
);
}
Widget _buildTabItem(AppTab tab, int index, bool isSelected) {
final canRemove = _selectedTabs.length > 1;
final child = ListTile(
leading: tab.navDestination.icon,
title: Text(tab.navDestination.label),
trailing: isSelected
? IconButton(
icon: const Icon(Icons.delete),
onPressed: canRemove ? () => _removeTab(tab) : null,
color: canRemove ? null : Theme.of(context).disabledColor,
tooltip: canRemove ? libL10n.delete : l10n.atLeastOneTab,
)
: IconButton(icon: const Icon(Icons.add), onPressed: () => _addTab(tab)),
onTap: isSelected && canRemove ? () => _removeTab(tab) : null,
);
return Card(
key: ValueKey(tab.name),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: isSelected ? ReorderableDragStartListener(index: index, child: child) : child,
);
}
void _onReorder(int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final tab = _selectedTabs.removeAt(oldIndex);
_selectedTabs.insert(newIndex, tab);
});
}
void _addTab(AppTab tab) {
setState(() {
_selectedTabs.add(tab);
});
}
void _removeTab(AppTab tab) {
if (_selectedTabs.length <= 1) {
context.showSnackBar(l10n.atLeastOneTab);
return;
}
setState(() {
_selectedTabs.remove(tab);
});
}
void _saveAndExit() {
Stores.setting.homeTabs.put(_selectedTabs);
context.pop();
}
void _resetToDefault() {
setState(() {
_selectedTabs = List<AppTab>.from(AppTab.values);
});
Stores.setting.homeTabs.put(_selectedTabs);
}
}

View File

@@ -18,6 +18,7 @@ import 'package:server_box/generated/l10n/l10n.dart';
import 'package:server_box/view/page/backup.dart';
import 'package:server_box/view/page/private_key/list.dart';
import 'package:server_box/view/page/server/connection_stats.dart';
import 'package:server_box/view/page/setting/entries/home_tabs.dart';
import 'package:server_box/view/page/setting/platform/android.dart';
import 'package:server_box/view/page/setting/platform/ios.dart';
import 'package:server_box/view/page/setting/platform/platform_pub.dart';