diff --git a/android/app/build.gradle b/android/app/build.gradle index 6f4546b01..431db75ad 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -49,7 +49,7 @@ android { applicationId 'com.exptech.dpip' minSdkVersion 26 targetSdkVersion 36 - versionCode 300103100 + versionCode 300103103 versionName flutterVersionName multiDexEnabled true resConfigs "en", "ko", "zh-rTW", "ja", "zh-rCN" diff --git a/lib/app/home/home_display_mode.dart b/lib/app/home/home_display_mode.dart new file mode 100644 index 000000000..1129d4c6d --- /dev/null +++ b/lib/app/home/home_display_mode.dart @@ -0,0 +1,6 @@ +enum HomeDisplaySection { + realtime, + radar, + forecast, + history, +} diff --git a/lib/app/home/page.dart b/lib/app/home/page.dart index bd4639b08..6552e0f47 100644 --- a/lib/app/home/page.dart +++ b/lib/app/home/page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:go_router/go_router.dart'; import 'package:i18n_extension/i18n_extension.dart'; +import 'package:provider/provider.dart'; import 'package:timezone/timezone.dart'; import 'package:dpip/api/exptech.dart'; @@ -20,16 +21,19 @@ import 'package:dpip/app/home/_widgets/mode_toggle_button.dart'; import 'package:dpip/app/home/_widgets/radar_card.dart'; import 'package:dpip/app/home/_widgets/thunderstorm_card.dart'; import 'package:dpip/app/home/_widgets/weather_header.dart'; +import 'package:dpip/app/settings/layout/page.dart'; import 'package:dpip/core/gps_location.dart'; import 'package:dpip/core/i18n.dart'; import 'package:dpip/core/preference.dart'; import 'package:dpip/core/providers.dart'; import 'package:dpip/global.dart'; import 'package:dpip/utils/constants.dart'; +import 'package:dpip/models/settings/ui.dart'; import 'package:dpip/utils/extensions/build_context.dart'; import 'package:dpip/utils/extensions/datetime.dart'; import 'package:dpip/utils/log.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; +import 'home_display_mode.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -257,7 +261,9 @@ class _HomePageState extends State with WidgetsBindingObserver { WidgetsBinding.instance.addPostFrameCallback((_) => _refresh()); } _wasVisible = isVisible; - + final homeSections = context.select>( + (model) => model.homeSections + ); final topPadding = MediaQuery.of(context).padding.top; WidgetsBinding.instance.addPostFrameCallback((_) { @@ -281,18 +287,41 @@ class _HomePageState extends State with WidgetsBindingObserver { onRefresh: _refresh, child: ListView( padding: EdgeInsets.only( - top: _locationButtonHeight != null ? 16 + topPadding + _locationButtonHeight! : 0, + top: _locationButtonHeight != null ? 24 + topPadding + _locationButtonHeight! : 0, ), children: [ _buildWeatherHeader(), - if (!_isLoading) ..._buildRealtimeInfo(), - _buildRadarMap(), - _buildHistoryTimeline(), + if (homeSections.isNotEmpty) ...[ + if (homeSections.contains(HomeDisplaySection.realtime)) + ..._buildRealtimeInfo(), + if (homeSections.contains(HomeDisplaySection.radar)) + _buildRadarMap(), + if (homeSections.contains(HomeDisplaySection.forecast)) + _buildForecast(), + if (homeSections.contains(HomeDisplaySection.history)) + _buildHistoryTimeline(), + ] else if (GlobalProviders.location.code != null) + Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Text( + '您還沒有啟用首頁區塊,請到設定選擇要顯示的內容。'.i18n, + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + FilledButton( + onPressed: () => context.push(SettingsLayoutPage.route), + child: Text('前往設定'.i18n), + ), + ], + ), + ), ], ), ), Positioned( - top: 16, + top: 24, left: 0, right: 0, child: SafeArea( @@ -332,13 +361,13 @@ class _HomePageState extends State with WidgetsBindingObserver { if (GlobalProviders.data.eew.isNotEmpty) ListView.builder( shrinkWrap: true, + padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), itemCount: GlobalProviders.data.eew.length, itemBuilder: (context, index) => Padding(padding: const EdgeInsets.all(16), child: EewCard(GlobalProviders.data.eew[index])), ), if (_thunderstorm != null) Padding(padding: const EdgeInsets.all(16), child: ThunderstormCard(_thunderstorm!)), - if (_forecast != null) ForecastCard(_forecast!), ]; } @@ -349,6 +378,11 @@ class _HomePageState extends State with WidgetsBindingObserver { ); } + Widget _buildForecast() { + if (_forecast == null) return const SizedBox.shrink(); + return ForecastCard(_forecast!); + } + Widget _buildHistoryTimeline() { return Builder( builder: (context) { diff --git a/lib/app/settings/layout/page.dart b/lib/app/settings/layout/page.dart new file mode 100644 index 000000000..5bd0b496e --- /dev/null +++ b/lib/app/settings/layout/page.dart @@ -0,0 +1,53 @@ +import 'package:dpip/core/i18n.dart'; +import 'package:dpip/models/settings/ui.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../../widgets/list/list_section.dart'; +import '../../home/home_display_mode.dart'; + +class SettingsLayoutPage extends StatelessWidget { + const SettingsLayoutPage({super.key}); + + static const route = '/settings/layout'; + + @override + Widget build(BuildContext context) { + return ListSection( + title: '首頁樣式'.i18n, + children: [ + Consumer( + builder: (context, model, child) { + final tiles = [ + SwitchListTile( + title: Text('圖卡資訊'.i18n), + value: model.isEnabled(HomeDisplaySection.realtime), + onChanged: (v) => model.toggleSection(HomeDisplaySection.realtime, v), + ), + SwitchListTile( + title: Text('雷達回波'.i18n), + value: model.isEnabled(HomeDisplaySection.radar), + onChanged: (v) => model.toggleSection(HomeDisplaySection.radar, v), + ), + SwitchListTile( + title: Text('天氣預報(24h)'.i18n), + value: model.isEnabled(HomeDisplaySection.forecast), + onChanged: (v) => model.toggleSection(HomeDisplaySection.forecast, v), + ), + SwitchListTile( + title: Text('歷史事件'.i18n), + value: model.isEnabled(HomeDisplaySection.history), + onChanged: (v) => model.toggleSection(HomeDisplaySection.history, v), + ), + ]; + return Column( + children: ListTile.divideTiles( + context: context, + tiles: tiles, + ).toList(), + ); + }, + ), + ], + ); + } +} diff --git a/lib/app/settings/page.dart b/lib/app/settings/page.dart index ed028544b..4d3687310 100644 --- a/lib/app/settings/page.dart +++ b/lib/app/settings/page.dart @@ -20,6 +20,8 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:simple_icons/simple_icons.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'layout/page.dart'; + class SettingsIndexPage extends StatelessWidget { const SettingsIndexPage({super.key}); @@ -47,6 +49,14 @@ class SettingsIndexPage extends StatelessWidget { final userInterface = ListSection( title: '介面'.i18n, children: [ + ListSectionTile( + icon: Symbols.grid_view_rounded, + title: '佈局'.i18n, + subtitle: Text('調整 DPIP 的佈局樣式'.i18n), + onTap: () { + context.push(SettingsLayoutPage.route); + }, + ), ListSectionTile( icon: Symbols.brush_rounded, title: '主題'.i18n, diff --git a/lib/app/welcome/4-permissions/page.dart b/lib/app/welcome/4-permissions/page.dart index 5d8ba3e8f..23f0b8319 100644 --- a/lib/app/welcome/4-permissions/page.dart +++ b/lib/app/welcome/4-permissions/page.dart @@ -310,7 +310,7 @@ class _WelcomePermissionPageState extends State with Widg announcement: true, carPlay: true, criticalAlert: true, - provisional: true, + provisional: false, ); if (iosSettings.criticalAlert == AppleNotificationSetting.enabled) { _isNotificationPermission = true; diff --git a/lib/core/preference.dart b/lib/core/preference.dart index 296bdc7c8..3618c6bc6 100644 --- a/lib/core/preference.dart +++ b/lib/core/preference.dart @@ -26,6 +26,7 @@ class PreferenceKeys { static const mapBase = 'pref:ui:map:base'; static const mapLayers = 'pref:ui:map:layers'; static const mapAutoZoom = 'pref:ui:map:autoZoom'; + static const homeDisplaySections = 'pref:ui:homeDisplaySections'; // #region Notification static const notifyEew = 'pref:notify:eew'; @@ -116,6 +117,9 @@ class Preference { static bool? get mapAutoZoom => instance.getBool(PreferenceKeys.mapAutoZoom); static set mapAutoZoom(bool? value) => instance.set(PreferenceKeys.mapAutoZoom, value); + + static List get homeDisplaySections => instance.getStringList(PreferenceKeys.homeDisplaySections) ?? []; + static set homeDisplaySections(List value) => instance.set(PreferenceKeys.homeDisplaySections, value); // #endregion // #region Notification diff --git a/lib/models/settings/ui.dart b/lib/models/settings/ui.dart index aeefc0fab..0c5a481d6 100644 --- a/lib/models/settings/ui.dart +++ b/lib/models/settings/ui.dart @@ -6,6 +6,8 @@ import 'package:dpip/core/preference.dart'; import 'package:dpip/utils/extensions/string.dart'; import 'package:flutter/material.dart'; +import '../../app/home/home_display_mode.dart'; + class SettingsUserInterfaceModel extends ChangeNotifier { void _log(String message) => log(message, name: 'SettingsUserInterfaceModel'); @@ -13,6 +15,8 @@ class SettingsUserInterfaceModel extends ChangeNotifier { int? get _themeColor => Preference.themeColor; Locale? get _locale => Preference.locale?.asLocale; bool get _useFahrenheit => Preference.useFahrenheit ?? false; + late Set homeSections; + final savedList = Preference.homeDisplaySections; ThemeMode get themeMode => ThemeMode.values.byName(_themeMode); void setThemeMode(ThemeMode value) { @@ -48,4 +52,26 @@ class SettingsUserInterfaceModel extends ChangeNotifier { _log('Changed ${PreferenceKeys.useFahrenheit} to ${Preference.useFahrenheit}'); notifyListeners(); } + + SettingsUserInterfaceModel() { + final saved = savedList + .map((s) => HomeDisplaySection.values + .cast() + .firstWhere((e) => e?.name == s, orElse: () => null)) + .whereType() + .toSet(); + homeSections = saved; + } + + bool isEnabled(HomeDisplaySection section) => homeSections.contains(section); + + void toggleSection(HomeDisplaySection section, bool enabled) { + if (enabled) { + homeSections.add(section); + } else { + homeSections.remove(section); + } + Preference.homeDisplaySections = homeSections.map((e) => e.name).toList(); + notifyListeners(); + } } diff --git a/lib/router.dart b/lib/router.dart index af7af8bf2..87f72bd90 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -12,6 +12,7 @@ import 'package:dpip/app/layout.dart'; import 'package:dpip/app/map/page.dart'; import 'package:dpip/app/settings/donate/page.dart'; import 'package:dpip/app/settings/layout.dart'; +import 'package:dpip/app/settings/layout/page.dart'; import 'package:dpip/app/settings/locale/page.dart'; import 'package:dpip/app/settings/locale/select/page.dart'; import 'package:dpip/app/settings/location/page.dart'; @@ -113,6 +114,7 @@ class HomeRoute extends GoRouteData with $HomeRoute { @TypedShellRoute( routes: >[ TypedGoRoute(path: '/settings'), + TypedGoRoute(path: '/settings/layout'), TypedGoRoute(path: '/settings/location'), TypedGoRoute(path: '/settings/location/select'), TypedGoRoute(path: '/settings/location/select/:city'), @@ -166,6 +168,7 @@ class SettingsShellRoute extends ShellRouteData { '/settings/location' => '所在地'.i18n, '/settings/location/select' => '新增地點'.i18n, final p when p?.startsWith('/settings/location/select/') == true => '新增地點'.i18n, + '/settings/layout' => '佈局'.i18n, '/settings/theme' => '主題'.i18n, '/settings/theme/select' => '主題'.i18n, '/settings/locale' => '語言'.i18n, @@ -235,6 +238,16 @@ class SettingsLocationSelectCityRoute extends GoRouteData with $SettingsLocation } } +class SettingsLayoutRoute extends GoRouteData with $SettingsLayoutRoute { + /// Creates a [SettingsLayoutRoute]. + const SettingsLayoutRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return const Material(child: SettingsLayoutPage()); + } +} + /// Settings theme route - displays theme settings. class SettingsThemeRoute extends GoRouteData with $SettingsThemeRoute { /// Creates a [SettingsThemeRoute].