From e51804fa7041a93aa5ef266b2f2dec289223cec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?lollipopkit=F0=9F=8F=B3=EF=B8=8F=E2=80=8D=E2=9A=A7?= =?UTF-8?q?=EF=B8=8F?= <10864310+lollipopkit@users.noreply.github.com> Date: Wed, 3 Sep 2025 01:05:03 +0800 Subject: [PATCH] new: custom tabs (#889) --- lib/data/model/app/tab.dart | 39 ++++++ lib/data/model/app/tab.g.dart | 52 ++++++++ lib/data/store/setting.dart | 69 ++++------ lib/generated/l10n/l10n.dart | 30 +++++ lib/generated/l10n/l10n_de.dart | 16 +++ lib/generated/l10n/l10n_en.dart | 16 +++ lib/generated/l10n/l10n_es.dart | 16 +++ lib/generated/l10n/l10n_fr.dart | 16 +++ lib/generated/l10n/l10n_id.dart | 16 +++ lib/generated/l10n/l10n_ja.dart | 15 +++ lib/generated/l10n/l10n_nl.dart | 17 +++ lib/generated/l10n/l10n_pt.dart | 16 +++ lib/generated/l10n/l10n_ru.dart | 16 +++ lib/generated/l10n/l10n_tr.dart | 16 +++ lib/generated/l10n/l10n_uk.dart | 16 +++ lib/generated/l10n/l10n_zh.dart | 30 +++++ lib/hive/hive_registrar.g.dart | 3 + lib/l10n/app_de.arb | 7 +- lib/l10n/app_en.arb | 7 +- lib/l10n/app_es.arb | 7 +- lib/l10n/app_fr.arb | 7 +- lib/l10n/app_id.arb | 7 +- lib/l10n/app_ja.arb | 7 +- lib/l10n/app_nl.arb | 7 +- lib/l10n/app_pt.arb | 7 +- lib/l10n/app_ru.arb | 7 +- lib/l10n/app_tr.arb | 7 +- lib/l10n/app_uk.arb | 7 +- lib/l10n/app_zh.arb | 7 +- lib/l10n/app_zh_tw.arb | 7 +- lib/view/page/home.dart | 29 ++++- lib/view/page/setting/entries/app.dart | 12 ++ lib/view/page/setting/entries/home_tabs.dart | 130 +++++++++++++++++++ lib/view/page/setting/entry.dart | 1 + pubspec.lock | 4 +- pubspec.yaml | 2 +- 36 files changed, 601 insertions(+), 67 deletions(-) create mode 100644 lib/data/model/app/tab.g.dart create mode 100644 lib/view/page/setting/entries/home_tabs.dart diff --git a/lib/data/model/app/tab.dart b/lib/data/model/app/tab.dart index 94ab9d0d..a8f440f7 100644 --- a/lib/data/model/app/tab.dart +++ b/lib/data/model/app/tab.dart @@ -1,5 +1,6 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:hive_ce_flutter/adapters.dart'; import 'package:icons_plus/icons_plus.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/view/page/server/tab/tab.dart'; @@ -8,10 +9,17 @@ import 'package:server_box/view/page/snippet/list.dart'; import 'package:server_box/view/page/ssh/tab.dart'; import 'package:server_box/view/page/storage/local.dart'; +part 'tab.g.dart'; + +@HiveType(typeId: 103) enum AppTab { + @HiveField(0) server, + @HiveField(1) ssh, + @HiveField(2) file, + @HiveField(3) snippet //settings, ; @@ -93,4 +101,35 @@ enum AppTab { static List get navRailDestinations { return AppTab.values.map((e) => e.navRailDestination).toList(); } + + + + /// Helper function to parse AppTab list from stored object + static List parseAppTabsFromObj(dynamic val) { + if (val is List) { + final tabs = []; + for (final e in val) { + final tab = _parseAppTabFromElement(e); + if (tab != null) { + tabs.add(tab); + } + } + if (tabs.isNotEmpty) return tabs; + } + return AppTab.values; + } + + /// Helper function to parse a single AppTab from various element types + static AppTab? _parseAppTabFromElement(dynamic e) { + if (e is AppTab) { + return e; + } else if (e is String) { + return AppTab.values.firstWhereOrNull((t) => t.name == e); + } else if (e is int) { + if (e >= 0 && e < AppTab.values.length) { + return AppTab.values[e]; + } + } + return null; + } } diff --git a/lib/data/model/app/tab.g.dart b/lib/data/model/app/tab.g.dart new file mode 100644 index 00000000..65a66eb7 --- /dev/null +++ b/lib/data/model/app/tab.g.dart @@ -0,0 +1,52 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tab.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class AppTabAdapter extends TypeAdapter { + @override + final typeId = 103; + + @override + AppTab read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return AppTab.server; + case 1: + return AppTab.ssh; + case 2: + return AppTab.file; + case 3: + return AppTab.snippet; + default: + return AppTab.server; + } + } + + @override + void write(BinaryWriter writer, AppTab obj) { + switch (obj) { + case AppTab.server: + writer.writeByte(0); + case AppTab.ssh: + writer.writeByte(1); + case AppTab.file: + writer.writeByte(2); + case AppTab.snippet: + writer.writeByte(3); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AppTabAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/data/store/setting.dart b/lib/data/store/setting.dart index 823ed980..16891e90 100644 --- a/lib/data/store/setting.dart +++ b/lib/data/store/setting.dart @@ -4,6 +4,7 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:server_box/data/model/app/menu/server_func.dart'; import 'package:server_box/data/model/app/net_view.dart'; import 'package:server_box/data/model/app/server_detail_card.dart'; +import 'package:server_box/data/model/app/tab.dart'; import 'package:server_box/data/model/ssh/virtual_key.dart'; import 'package:server_box/data/res/default.dart'; @@ -22,10 +23,7 @@ class SettingStore extends HiveStore { // late final launchPage = property('launchPage', Defaults.launchPageIdx); /// Disk view: amount / IO - late final serverTabPreferDiskAmount = propertyDefault( - 'serverTabPreferDiskAmount', - false, - ); + late final serverTabPreferDiskAmount = propertyDefault('serverTabPreferDiskAmount', false); /// Bigger for bigger font size /// 1.0 means 100% @@ -70,20 +68,14 @@ class SettingStore extends HiveStore { late final locale = propertyDefault('locale', ''); // SSH virtual key (ctrl | alt) auto turn off - late final sshVirtualKeyAutoOff = propertyDefault( - 'sshVirtualKeyAutoOff', - true, - ); + late final sshVirtualKeyAutoOff = propertyDefault('sshVirtualKeyAutoOff', true); late final editorFontSize = propertyDefault('editorFontSize', 12.5); // Editor theme late final editorTheme = propertyDefault('editorTheme', Defaults.editorTheme); - late final editorDarkTheme = propertyDefault( - 'editorDarkTheme', - Defaults.editorDarkTheme, - ); + late final editorDarkTheme = propertyDefault('editorDarkTheme', Defaults.editorDarkTheme); late final fullScreen = propertyDefault('fullScreen', false); @@ -113,29 +105,20 @@ class SettingStore extends HiveStore { ); // Only valid on iOS - late final autoUpdateHomeWidget = propertyDefault( - 'autoUpdateHomeWidget', - isIOS, - ); + late final autoUpdateHomeWidget = propertyDefault('autoUpdateHomeWidget', isIOS); late final autoCheckAppUpdate = propertyDefault('autoCheckAppUpdate', true); /// Display server tab function buttons on the bottom of each server card if [true] /// /// Otherwise, display them on the top of server detail page - late final moveServerFuncs = propertyDefault( - 'moveOutServerTabFuncBtns', - false, - ); + late final moveServerFuncs = propertyDefault('moveOutServerTabFuncBtns', false); /// Whether use `rm -r` to delete directory on SFTP late final sftpRmrDir = propertyDefault('sftpRmrDir', false); /// Whether use system's primary color as the app's primary color - late final useSystemPrimaryColor = propertyDefault( - 'useSystemPrimaryColor', - false, - ); + late final useSystemPrimaryColor = propertyDefault('useSystemPrimaryColor', false); /// Only valid on iOS / Android / Windows late final useBioAuth = propertyDefault('useBioAuth', false); @@ -151,10 +134,7 @@ class SettingStore extends HiveStore { late final sftpOpenLastPath = propertyDefault('sftpOpenLastPath', true); /// Show folders first in SFTP file browser - late final sftpShowFoldersFirst = propertyDefault( - 'sftpShowFoldersFirst', - true, - ); + late final sftpShowFoldersFirst = propertyDefault('sftpShowFoldersFirst', true); /// Show tip of suspend late final showSuspendTip = propertyDefault('showSuspendTip', true); @@ -162,10 +142,7 @@ class SettingStore extends HiveStore { /// Whether collapse UI items by default late final collapseUIDefault = propertyDefault('collapseUIDefault', true); - late final serverFuncBtns = listProperty( - 'serverBtns', - defaultValue: ServerFuncBtn.defaultIdxs, - ); + late final serverFuncBtns = listProperty('serverBtns', defaultValue: ServerFuncBtn.defaultIdxs); /// Docker is more popular than podman, set to `false` to use docker late final usePodman = propertyDefault('usePodman', false); @@ -180,16 +157,10 @@ class SettingStore extends HiveStore { late final containerParseStat = propertyDefault('containerParseStat', true); /// Auto refresh container status - late final containerAutoRefresh = propertyDefault( - 'containerAutoRefresh', - true, - ); + late final containerAutoRefresh = propertyDefault('containerAutoRefresh', true); /// Use double column servers page on Desktop - late final doubleColumnServersPage = propertyDefault( - 'doubleColumnServersPage', - true, - ); + late final doubleColumnServersPage = propertyDefault('doubleColumnServersPage', true); /// Ignore local network device (eg: br-xxx, ovs-system...) /// when building traffic view on server tab @@ -244,8 +215,7 @@ class SettingStore extends HiveStore { /// Record the position and size of the window. late final windowState = property( 'windowState', - fromObj: (raw) => - WindowState.fromJson(jsonDecode(raw as String) as Map), + fromObj: (raw) => WindowState.fromJson(jsonDecode(raw as String) as Map), toObj: (state) => state == null ? null : jsonEncode(state.toJson()), ); @@ -258,10 +228,7 @@ class SettingStore extends HiveStore { late final sftpEditor = propertyDefault('sftpEditor', ''); /// Preferred terminal emulator command on desktop - late final desktopTerminal = propertyDefault( - 'desktopTerminal', - 'x-terminal-emulator', - ); + late final desktopTerminal = propertyDefault('desktopTerminal', 'x-terminal-emulator'); /// Run foreground service on Android, if the SSH terminal is running late final fgService = propertyDefault('fgService', false); @@ -280,4 +247,14 @@ class SettingStore extends HiveStore { /// Whether to read SSH config from ~/.ssh/config on first time late final firstTimeReadSSHCfg = propertyDefault('firstTimeReadSSHCfg', true); + + /// Tabs at home page + late final homeTabs = listProperty( + 'homeTabs', + defaultValue: AppTab.values, + fromObj: AppTab.parseAppTabsFromObj, + toObj: (val) { + return val?.map((e) => e.name).toList() ?? []; + }, + ); } diff --git a/lib/generated/l10n/l10n.dart b/lib/generated/l10n/l10n.dart index 0d28d5ee..e5695558 100644 --- a/lib/generated/l10n/l10n.dart +++ b/lib/generated/l10n/l10n.dart @@ -1699,6 +1699,36 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Are you sure you want to clear connection statistics for server \"{serverName}\"? This action cannot be undone.'** String clearServerStatsContent(String serverName); + + /// No description provided for @homeTabs. + /// + /// In en, this message translates to: + /// **'Home Tabs'** + String get homeTabs; + + /// No description provided for @homeTabsCustomizeDesc. + /// + /// In en, this message translates to: + /// **'Customize which tabs appear on the home page and their order'** + String get homeTabsCustomizeDesc; + + /// No description provided for @reset. + /// + /// In en, this message translates to: + /// **'Reset'** + String get reset; + + /// No description provided for @availableTabs. + /// + /// In en, this message translates to: + /// **'Available Tabs'** + String get availableTabs; + + /// No description provided for @atLeastOneTab. + /// + /// In en, this message translates to: + /// **'At least one tab must be selected'** + String get atLeastOneTab; } class _AppLocalizationsDelegate diff --git a/lib/generated/l10n/l10n_de.dart b/lib/generated/l10n/l10n_de.dart index 6410254c..6040ef6a 100644 --- a/lib/generated/l10n/l10n_de.dart +++ b/lib/generated/l10n/l10n_de.dart @@ -895,4 +895,20 @@ class AppLocalizationsDe extends AppLocalizations { String clearServerStatsContent(String serverName) { return 'Sind Sie sicher, dass Sie die Verbindungsstatistiken für Server \"$serverName\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.'; } + + @override + String get homeTabs => 'Home-Tabs'; + + @override + String get homeTabsCustomizeDesc => + 'Passen Sie an, welche Tabs auf der Startseite angezeigt werden und ihre Reihenfolge'; + + @override + String get reset => 'Zurücksetzen'; + + @override + String get availableTabs => 'Verfügbare Tabs'; + + @override + String get atLeastOneTab => 'Mindestens ein Tab muss ausgewählt sein'; } diff --git a/lib/generated/l10n/l10n_en.dart b/lib/generated/l10n/l10n_en.dart index 808d4ff5..dfa792bd 100644 --- a/lib/generated/l10n/l10n_en.dart +++ b/lib/generated/l10n/l10n_en.dart @@ -887,4 +887,20 @@ class AppLocalizationsEn extends AppLocalizations { String clearServerStatsContent(String serverName) { return 'Are you sure you want to clear connection statistics for server \"$serverName\"? This action cannot be undone.'; } + + @override + String get homeTabs => 'Home Tabs'; + + @override + String get homeTabsCustomizeDesc => + 'Customize which tabs appear on the home page and their order'; + + @override + String get reset => 'Reset'; + + @override + String get availableTabs => 'Available Tabs'; + + @override + String get atLeastOneTab => 'At least one tab must be selected'; } diff --git a/lib/generated/l10n/l10n_es.dart b/lib/generated/l10n/l10n_es.dart index 4ca13cf2..2ab073d0 100644 --- a/lib/generated/l10n/l10n_es.dart +++ b/lib/generated/l10n/l10n_es.dart @@ -897,4 +897,20 @@ class AppLocalizationsEs extends AppLocalizations { String clearServerStatsContent(String serverName) { return '¿Estás seguro de que quieres limpiar las estadísticas de conexión del servidor \"$serverName\"? Esta acción no se puede deshacer.'; } + + @override + String get homeTabs => 'Pestañas de inicio'; + + @override + String get homeTabsCustomizeDesc => + 'Personaliza qué pestañas aparecen en la página de inicio y su orden'; + + @override + String get reset => 'Restablecer'; + + @override + String get availableTabs => 'Pestañas disponibles'; + + @override + String get atLeastOneTab => 'Al menos una pestaña debe estar seleccionada'; } diff --git a/lib/generated/l10n/l10n_fr.dart b/lib/generated/l10n/l10n_fr.dart index cf2256ab..bf647155 100644 --- a/lib/generated/l10n/l10n_fr.dart +++ b/lib/generated/l10n/l10n_fr.dart @@ -900,4 +900,20 @@ class AppLocalizationsFr extends AppLocalizations { String clearServerStatsContent(String serverName) { return 'Êtes-vous sûr de vouloir effacer les statistiques de connexion du serveur \"$serverName\" ? Cette action ne peut pas être annulée.'; } + + @override + String get homeTabs => 'Onglets d\'accueil'; + + @override + String get homeTabsCustomizeDesc => + 'Personnalisez les onglets qui apparaissent sur la page d\'accueil et leur ordre'; + + @override + String get reset => 'Réinitialiser'; + + @override + String get availableTabs => 'Onglets disponibles'; + + @override + String get atLeastOneTab => 'Au moins un onglet doit être sélectionné'; } diff --git a/lib/generated/l10n/l10n_id.dart b/lib/generated/l10n/l10n_id.dart index 2cf3a4dd..b4eed71c 100644 --- a/lib/generated/l10n/l10n_id.dart +++ b/lib/generated/l10n/l10n_id.dart @@ -887,4 +887,20 @@ class AppLocalizationsId extends AppLocalizations { String clearServerStatsContent(String serverName) { return 'Apakah Anda yakin ingin menghapus statistik koneksi untuk server \"$serverName\"? Tindakan ini tidak dapat dibatalkan.'; } + + @override + String get homeTabs => 'Tab Beranda'; + + @override + String get homeTabsCustomizeDesc => + 'Sesuaikan tab mana yang muncul di halaman beranda dan urutannya'; + + @override + String get reset => 'Reset'; + + @override + String get availableTabs => 'Tab Tersedia'; + + @override + String get atLeastOneTab => 'Setidaknya satu tab harus dipilih'; } diff --git a/lib/generated/l10n/l10n_ja.dart b/lib/generated/l10n/l10n_ja.dart index 833fc375..65f164d9 100644 --- a/lib/generated/l10n/l10n_ja.dart +++ b/lib/generated/l10n/l10n_ja.dart @@ -861,4 +861,19 @@ class AppLocalizationsJa extends AppLocalizations { String clearServerStatsContent(String serverName) { return 'サーバー\"$serverName\"の接続統計を削除してもよろしいですか?この操作は元に戻せません。'; } + + @override + String get homeTabs => 'ホームタブ'; + + @override + String get homeTabsCustomizeDesc => 'ホームページに表示するタブとその順序をカスタマイズします'; + + @override + String get reset => 'リセット'; + + @override + String get availableTabs => '利用可能なタブ'; + + @override + String get atLeastOneTab => '少なくとも1つのタブを選択する必要があります'; } diff --git a/lib/generated/l10n/l10n_nl.dart b/lib/generated/l10n/l10n_nl.dart index d49a6fdf..75089c6b 100644 --- a/lib/generated/l10n/l10n_nl.dart +++ b/lib/generated/l10n/l10n_nl.dart @@ -893,4 +893,21 @@ class AppLocalizationsNl extends AppLocalizations { String clearServerStatsContent(String serverName) { return 'Weet u zeker dat u de verbindingsstatistieken voor server \"$serverName\" wilt wissen? Deze actie kan niet ongedaan worden gemaakt.'; } + + @override + String get homeTabs => 'Home-tabbladen'; + + @override + String get homeTabsCustomizeDesc => + 'Pas aan welke tabbladen op de startpagina worden weergegeven en hun volgorde'; + + @override + String get reset => 'Resetten'; + + @override + String get availableTabs => 'Beschikbare tabbladen'; + + @override + String get atLeastOneTab => + 'Er moet minimaal één tabblad worden geselecteerd'; } diff --git a/lib/generated/l10n/l10n_pt.dart b/lib/generated/l10n/l10n_pt.dart index 27413cca..c9ffa858 100644 --- a/lib/generated/l10n/l10n_pt.dart +++ b/lib/generated/l10n/l10n_pt.dart @@ -890,4 +890,20 @@ class AppLocalizationsPt extends AppLocalizations { String clearServerStatsContent(String serverName) { return 'Tem certeza de que deseja limpar as estatísticas de conexão para o servidor \"$serverName\"? Esta ação não pode ser desfeita.'; } + + @override + String get homeTabs => 'Abas iniciais'; + + @override + String get homeTabsCustomizeDesc => + 'Personalize quais abas aparecem na página inicial e sua ordem'; + + @override + String get reset => 'Redefinir'; + + @override + String get availableTabs => 'Abas disponíveis'; + + @override + String get atLeastOneTab => 'Pelo menos uma aba deve ser selecionada'; } diff --git a/lib/generated/l10n/l10n_ru.dart b/lib/generated/l10n/l10n_ru.dart index 02435104..25e2a812 100644 --- a/lib/generated/l10n/l10n_ru.dart +++ b/lib/generated/l10n/l10n_ru.dart @@ -892,4 +892,20 @@ class AppLocalizationsRu extends AppLocalizations { String clearServerStatsContent(String serverName) { return 'Вы уверены, что хотите очистить статистику соединений для сервера \"$serverName\"? Это действие не может быть отменено.'; } + + @override + String get homeTabs => 'Вкладки дома'; + + @override + String get homeTabsCustomizeDesc => + 'Настройте, какие вкладки появляются на главной странице и их порядок'; + + @override + String get reset => 'Сброс'; + + @override + String get availableTabs => 'Доступные вкладки'; + + @override + String get atLeastOneTab => 'Должна быть выбрана хотя бы одна вкладка'; } diff --git a/lib/generated/l10n/l10n_tr.dart b/lib/generated/l10n/l10n_tr.dart index c61208c1..1d947637 100644 --- a/lib/generated/l10n/l10n_tr.dart +++ b/lib/generated/l10n/l10n_tr.dart @@ -887,4 +887,20 @@ class AppLocalizationsTr extends AppLocalizations { String clearServerStatsContent(String serverName) { return '\"$serverName\" sunucusu için bağlantı istatistiklerini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.'; } + + @override + String get homeTabs => 'Ana Sayfa Sekmeleri'; + + @override + String get homeTabsCustomizeDesc => + 'Ana sayfada görünecek sekmeleri ve sıralarını özelleştirin'; + + @override + String get reset => 'Sıfırla'; + + @override + String get availableTabs => 'Mevcut Sekmeler'; + + @override + String get atLeastOneTab => 'En az bir sekme seçilmelidir'; } diff --git a/lib/generated/l10n/l10n_uk.dart b/lib/generated/l10n/l10n_uk.dart index 4274bf24..25fd9681 100644 --- a/lib/generated/l10n/l10n_uk.dart +++ b/lib/generated/l10n/l10n_uk.dart @@ -893,4 +893,20 @@ class AppLocalizationsUk extends AppLocalizations { String clearServerStatsContent(String serverName) { return 'Ви впевнені, що хочете очистити статистику з\'єднань для сервера \"$serverName\"? Цю дію не можна скасувати.'; } + + @override + String get homeTabs => 'Домашні вкладки'; + + @override + String get homeTabsCustomizeDesc => + 'Налаштуйте, які вкладки відображаються на головній сторінці та їх порядок'; + + @override + String get reset => 'Скинути'; + + @override + String get availableTabs => 'Доступні вкладки'; + + @override + String get atLeastOneTab => 'Потрібно вибрати принаймні одну вкладку'; } diff --git a/lib/generated/l10n/l10n_zh.dart b/lib/generated/l10n/l10n_zh.dart index 6cd9a14c..c2ce3137 100644 --- a/lib/generated/l10n/l10n_zh.dart +++ b/lib/generated/l10n/l10n_zh.dart @@ -846,6 +846,21 @@ class AppLocalizationsZh extends AppLocalizations { String clearServerStatsContent(String serverName) { return '确定要清空服务器 \"$serverName\" 的连接统计数据吗?此操作无法撤销。'; } + + @override + String get homeTabs => '主页标签'; + + @override + String get homeTabsCustomizeDesc => '自定义主页上显示的标签及其顺序'; + + @override + String get reset => '重置'; + + @override + String get availableTabs => '可用标签'; + + @override + String get atLeastOneTab => '至少需要选择一个标签'; } /// The translations for Chinese, as used in Taiwan (`zh_TW`). @@ -1690,4 +1705,19 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { String clearServerStatsContent(String serverName) { return '確定要清空伺服器 \"$serverName\" 的連線統計資料嗎?此操作無法撤銷。'; } + + @override + String get homeTabs => '主頁標籤'; + + @override + String get homeTabsCustomizeDesc => '自訂主頁上顯示的標籤及其順序'; + + @override + String get reset => '重置'; + + @override + String get availableTabs => '可用標籤'; + + @override + String get atLeastOneTab => '至少需要選擇一個標籤'; } diff --git a/lib/hive/hive_registrar.g.dart b/lib/hive/hive_registrar.g.dart index 38bfc8ab..b1da7739 100644 --- a/lib/hive/hive_registrar.g.dart +++ b/lib/hive/hive_registrar.g.dart @@ -3,11 +3,13 @@ // Check in to version control import 'package:hive_ce/hive.dart'; +import 'package:server_box/data/model/app/tab.dart'; import 'package:server_box/data/model/server/connection_stat.dart'; import 'package:server_box/hive/hive_adapters.dart'; extension HiveRegistrar on HiveInterface { void registerAdapters() { + registerAdapter(AppTabAdapter()); registerAdapter(ConnectionResultAdapter()); registerAdapter(ConnectionStatAdapter()); registerAdapter(NetViewTypeAdapter()); @@ -25,6 +27,7 @@ extension HiveRegistrar on HiveInterface { extension IsolatedHiveRegistrar on IsolatedHiveInterface { void registerAdapters() { + registerAdapter(AppTabAdapter()); registerAdapter(ConnectionResultAdapter()); registerAdapter(ConnectionStatAdapter()); registerAdapter(NetViewTypeAdapter()); diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 72860e9c..b7610cb7 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -276,5 +276,10 @@ "type": "String" } } - } + }, + "homeTabs": "Home-Tabs", + "homeTabsCustomizeDesc": "Passen Sie an, welche Tabs auf der Startseite angezeigt werden und ihre Reihenfolge", + "reset": "Zurücksetzen", + "availableTabs": "Verfügbare Tabs", + "atLeastOneTab": "Mindestens ein Tab muss ausgewählt sein" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a9e6b464..c4e79510 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -276,5 +276,10 @@ "type": "String" } } - } + }, + "homeTabs": "Home Tabs", + "homeTabsCustomizeDesc": "Customize which tabs appear on the home page and their order", + "reset": "Reset", + "availableTabs": "Available Tabs", + "atLeastOneTab": "At least one tab must be selected" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index acea4ac5..4c47eb89 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -276,5 +276,10 @@ "type": "String" } } - } + }, + "homeTabs": "Pestañas de inicio", + "homeTabsCustomizeDesc": "Personaliza qué pestañas aparecen en la página de inicio y su orden", + "reset": "Restablecer", + "availableTabs": "Pestañas disponibles", + "atLeastOneTab": "Al menos una pestaña debe estar seleccionada" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 849a8dce..ee2c93b9 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -276,5 +276,10 @@ "type": "String" } } - } + }, + "homeTabs": "Onglets d'accueil", + "homeTabsCustomizeDesc": "Personnalisez les onglets qui apparaissent sur la page d'accueil et leur ordre", + "reset": "Réinitialiser", + "availableTabs": "Onglets disponibles", + "atLeastOneTab": "Au moins un onglet doit être sélectionné" } \ No newline at end of file diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index b93673c5..e747cf33 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -276,5 +276,10 @@ "type": "String" } } - } + }, + "homeTabs": "Tab Beranda", + "homeTabsCustomizeDesc": "Sesuaikan tab mana yang muncul di halaman beranda dan urutannya", + "reset": "Reset", + "availableTabs": "Tab Tersedia", + "atLeastOneTab": "Setidaknya satu tab harus dipilih" } \ No newline at end of file diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 27d914b8..94eebd9d 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -276,5 +276,10 @@ "type": "String" } } - } + }, + "homeTabs": "ホームタブ", + "homeTabsCustomizeDesc": "ホームページに表示するタブとその順序をカスタマイズします", + "reset": "リセット", + "availableTabs": "利用可能なタブ", + "atLeastOneTab": "少なくとも1つのタブを選択する必要があります" } \ No newline at end of file diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 63a6dc33..29612fd4 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -276,5 +276,10 @@ "type": "String" } } - } + }, + "homeTabs": "Home-tabbladen", + "homeTabsCustomizeDesc": "Pas aan welke tabbladen op de startpagina worden weergegeven en hun volgorde", + "reset": "Resetten", + "availableTabs": "Beschikbare tabbladen", + "atLeastOneTab": "Er moet minimaal één tabblad worden geselecteerd" } \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 37ac01ef..a6f79beb 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -276,5 +276,10 @@ "type": "String" } } - } + }, + "homeTabs": "Abas iniciais", + "homeTabsCustomizeDesc": "Personalize quais abas aparecem na página inicial e sua ordem", + "reset": "Redefinir", + "availableTabs": "Abas disponíveis", + "atLeastOneTab": "Pelo menos uma aba deve ser selecionada" } \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index a14e6aab..cc4191b2 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -276,5 +276,10 @@ "type": "String" } } - } + }, + "homeTabs": "Вкладки дома", + "homeTabsCustomizeDesc": "Настройте, какие вкладки появляются на главной странице и их порядок", + "reset": "Сброс", + "availableTabs": "Доступные вкладки", + "atLeastOneTab": "Должна быть выбрана хотя бы одна вкладка" } \ No newline at end of file diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index ff1cef3e..80692cf3 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -276,5 +276,10 @@ "type": "String" } } - } + }, + "homeTabs": "Ana Sayfa Sekmeleri", + "homeTabsCustomizeDesc": "Ana sayfada görünecek sekmeleri ve sıralarını özelleştirin", + "reset": "Sıfırla", + "availableTabs": "Mevcut Sekmeler", + "atLeastOneTab": "En az bir sekme seçilmelidir" } \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 0e19625d..a271d5ab 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -276,5 +276,10 @@ "type": "String" } } - } + }, + "homeTabs": "Домашні вкладки", + "homeTabsCustomizeDesc": "Налаштуйте, які вкладки відображаються на головній сторінці та їх порядок", + "reset": "Скинути", + "availableTabs": "Доступні вкладки", + "atLeastOneTab": "Потрібно вибрати принаймні одну вкладку" } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 1796c032..9b70a2fb 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -276,5 +276,10 @@ "type": "String" } } - } + }, + "homeTabs": "主页标签", + "homeTabsCustomizeDesc": "自定义主页上显示的标签及其顺序", + "reset": "重置", + "availableTabs": "可用标签", + "atLeastOneTab": "至少需要选择一个标签" } \ No newline at end of file diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb index 835de0a8..cab61a4f 100644 --- a/lib/l10n/app_zh_tw.arb +++ b/lib/l10n/app_zh_tw.arb @@ -276,5 +276,10 @@ "type": "String" } } - } + }, + "homeTabs": "主頁標籤", + "homeTabsCustomizeDesc": "自訂主頁上顯示的標籤及其順序", + "reset": "重置", + "availableTabs": "可用標籤", + "atLeastOneTab": "至少需要選擇一個標籤" } \ No newline at end of file diff --git a/lib/view/page/home.dart b/lib/view/page/home.dart index 9daf07cd..23da02ff 100644 --- a/lib/view/page/home.dart +++ b/lib/view/page/home.dart @@ -33,6 +33,7 @@ class _HomePageState extends ConsumerState late final _notifier = ref.read(serversNotifierProvider.notifier); late final _provider = ref.read(serversNotifierProvider); + late List _tabs = Stores.setting.homeTabs.fetch(); @override void dispose() { @@ -51,13 +52,30 @@ class _HomePageState extends ConsumerState 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 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 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 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 void _onDestinationSelected(int index) { if (_selectIndex.value == index) return; + if (index < 0 || index >= _tabs.length) return; _selectIndex.value = index; _switchingPage = true; _pageController.animateToPage( diff --git a/lib/view/page/setting/entries/app.dart b/lib/view/page/setting/entries/app.dart index b8beb865..16277994 100644 --- a/lib/view/page/setting/entries/app.dart +++ b/lib/view/page/setting/entries/app.dart @@ -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); + }, + ); + } } diff --git a/lib/view/page/setting/entries/home_tabs.dart b/lib/view/page/setting/entries/home_tabs.dart new file mode 100644 index 00000000..9ea4b720 --- /dev/null +++ b/lib/view/page/setting/entries/home_tabs.dart @@ -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 createState() => _HomeTabsConfigPageState(); +} + +class _HomeTabsConfigPageState extends ConsumerState { + final _availableTabs = AppTab.values; + var _selectedTabs = List.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.from(AppTab.values); + }); + Stores.setting.homeTabs.put(_selectedTabs); + } +} diff --git a/lib/view/page/setting/entry.dart b/lib/view/page/setting/entry.dart index aabb9dd0..5fb5f166 100644 --- a/lib/view/page/setting/entry.dart +++ b/lib/view/page/setting/entry.dart @@ -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'; diff --git a/pubspec.lock b/pubspec.lock index 2f42bfa1..e3050788 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -497,8 +497,8 @@ packages: dependency: "direct main" description: path: "." - ref: "v1.0.345" - resolved-ref: "1b797643ef7603dd825caf96a6c57b88dbd23c34" + ref: "v1.0.346" + resolved-ref: f277b7a4259e45889320ef6d80ab320662558784 url: "https://github.com/lppcg/fl_lib" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index c8c3175f..d8db9c61 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: fl_lib: git: url: https://github.com/lppcg/fl_lib - ref: v1.0.345 + ref: v1.0.346 flutter_gbk2utf8: ^1.0.1 get_it: ^8.2.0