diff --git a/lib/app/home/page.dart b/lib/app/home/page.dart index 3855d9865..dadbcb0a9 100644 --- a/lib/app/home/page.dart +++ b/lib/app/home/page.dart @@ -312,7 +312,7 @@ class _HomePageState extends State with WidgetsBindingObserver { _wasVisible = isVisible; final homeSections = context - .select>( + .select>( (model) => model.homeSections, ); @@ -427,55 +427,65 @@ class _HomePageState extends State with WidgetsBindingObserver { ); } - Widget _buildContentSection(Set homeSections) { - return Column( - children: [ - // 拖曳指示器 - Container( - margin: const EdgeInsets.only(top: 12, bottom: 8), - width: 40, - height: 4, - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(2), - ), + Widget _buildContentSection(List homeSections) { + final children = [ + // 拖曳指示器 + Container( + margin: const EdgeInsets.only(top: 12, bottom: 8), + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(2), ), - // 實時警報 - if (!_isLoading) ..._buildRealtimeInfo(), - // 其他區塊 - if (homeSections.isNotEmpty) ...[ - if (homeSections.contains(HomeDisplaySection.radar)) _buildRadarMap(), - if (homeSections.contains(HomeDisplaySection.forecast)) - _buildForecast(), - if (!_isLoading && - homeSections.contains(HomeDisplaySection.wind) && - _weather != null) - _buildWindCard(), - _buildCommunityCards(), - 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, - style: TextStyle(color: Colors.white.withValues(alpha: 0.8)), - ), - const SizedBox(height: 12), - FilledButton( - onPressed: () => SettingsLayoutRoute().push(context), - child: Text('前往設定'.i18n), - ), - ], - ), + ), + // 實時警報 + if (!_isLoading) ..._buildRealtimeInfo(), + ]; + + for (final section in homeSections) { + switch (section) { + case HomeDisplaySection.radar: + children.add(_buildRadarMap()); + break; + case HomeDisplaySection.forecast: + children.add(_buildForecast()); + break; + case HomeDisplaySection.wind: + if (!_isLoading && _weather != null) children.add(_buildWindCard()); + break; + case HomeDisplaySection.history: + children.add(_buildHistoryTimeline()); + break; + case HomeDisplaySection.community: + children.add(_buildCommunityCards()); + break; + } + } + if (homeSections.isEmpty && GlobalProviders.location.code != null) { + children.add( + Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Text( + '您還沒有啟用首頁區塊,請到設定選擇要顯示的內容。'.i18n, + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white.withValues(alpha: 0.8)), + ), + const SizedBox(height: 12), + FilledButton( + onPressed: () => SettingsLayoutRoute().push(context), + child: Text('前往設定'.i18n), + ), + ], ), - // 底部安全區域 - SizedBox(height: MediaQuery.of(context).padding.bottom + 16), - ], - ); + ), + ); + }; + children.add(SizedBox(height: MediaQuery.of(context).padding.bottom + 16)); + + return Column(children: children); } Widget _buildCommunityCards() { diff --git a/lib/app/settings/layout/page.dart b/lib/app/settings/layout/page.dart index d6c3ca88f..966956d4a 100644 --- a/lib/app/settings/layout/page.dart +++ b/lib/app/settings/layout/page.dart @@ -12,100 +12,142 @@ class SettingsLayoutPage extends StatelessWidget { @override Widget build(BuildContext context) { - return ListView( - padding: EdgeInsets.only( - top: 16, - bottom: MediaQuery.of(context).padding.bottom + 16, - ), - children: [ - _buildHeader(context), - const SizedBox(height: 16), - SegmentedList( + return Consumer( + builder: (context, model, child) { + final enabledSections = model.homeSections; + final disabledSections = HomeDisplaySection.values + .where((s) => !enabledSections.contains(s)) + .toList(); + + return ListView( + padding: EdgeInsets.only( + top: 16, + bottom: MediaQuery.of(context).padding.bottom + 16, + ), children: [ - Selector( - selector: (context, model) => model.isEnabled(.radar), - builder: (context, isEnabled, child) { - return SegmentedListTile( - isFirst: true, - leading: ContainedIcon( - Symbols.radar_rounded, - color: Colors.blueAccent, - ), - title: Text('雷達回波'.i18n), - subtitle: Text('顯示即時雷達回波圖'.i18n), - trailing: Switch( - value: isEnabled, - onChanged: (value) { - context.userInterface.toggleSection(.radar, value); - }, - ), - ); - }, - ), - Selector( - selector: (context, model) => model.isEnabled(.forecast), - builder: (context, isEnabled, child) { - return SegmentedListTile( - leading: ContainedIcon( - Symbols.radar_rounded, - color: Colors.orangeAccent, - ), - title: Text('天氣預報'.i18n), - subtitle: Text('顯示未來 24 小時的天氣預報'.i18n), - trailing: Switch( - value: isEnabled, - onChanged: (value) { - context.userInterface.toggleSection(.forecast, value); - }, - ), - ); - }, - ), - Selector( - selector: (context, model) => model.isEnabled(.wind), - builder: (context, isEnabled, child) { - return SegmentedListTile( - leading: ContainedIcon( - Symbols.wind_power_rounded, - color: Colors.orangeAccent, - ), - title: Text('風向'.i18n), - subtitle: Text('顯示風向與風力級數'.i18n), - trailing: Switch( - value: isEnabled, - onChanged: (value) { - context.userInterface.toggleSection(.wind, value); - }, - ), - ); - }, - ), - Selector( - selector: (context, model) => model.isEnabled(.history), - builder: (context, isEnabled, child) { - return SegmentedListTile( - isLast: true, - leading: ContainedIcon( - Symbols.history_rounded, - color: Colors.greenAccent, + _buildHeader(context), + const SizedBox(height: 16), + if (enabledSections.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Text( + '顯示中'.i18n, + style: context.texts.labelLarge?.copyWith( + color: context.colors.primary, ), - title: Text('歷史事件'.i18n), - subtitle: Text('顯示地震與災害歷史紀錄'.i18n), - trailing: Switch( - value: isEnabled, - onChanged: (value) { - context.userInterface.toggleSection(.history, value); - }, + ), + ), + ReorderableListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: enabledSections.length, + onReorder: model.reorderSection, + proxyDecorator: (child, index, animation) { + return Material( + color: Colors.transparent, + child: child, + ); + }, + itemBuilder: (context, index) { + final section = enabledSections[index]; + final details = _getSectionDetails(section); + return _buildSectionCard( + context, + key: ValueKey(section), + icon: details.icon, + iconColor: details.color, + title: details.title, + subtitle: details.subtitle, + value: true, + onChanged: (section == HomeDisplaySection.history || + section == HomeDisplaySection.community) + ? null + : (v) => model.toggleSection(section, v), + isReorderable: true, + ); + }, + ), + ], + if (disabledSections.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Text( + '已隱藏'.i18n, + style: context.texts.labelLarge?.copyWith( + color: context.colors.onSurfaceVariant, ), - ); - }, - ), + ), + ), + ...disabledSections.map( + (section) { + final details = _getSectionDetails(section); + return _buildSectionCard( + context, + key: ValueKey(section), + icon: details.icon, + iconColor: details.color, + title: details.title, + subtitle: details.subtitle, + value: false, + onChanged: (v) => model.toggleSection(section, v), + isReorderable: false, + ); + }, + ), + ], ], - ), - ], + ); + }, ); } + ({IconData icon, Color color, String title, String subtitle}) + _getSectionDetails(HomeDisplaySection section) { + switch (section) { + case HomeDisplaySection.radar: + return ( + icon: Symbols.radar_rounded, + color: Colors.blue, + title: '雷達回波'.i18n, + subtitle: '顯示即時雷達回波圖'.i18n, + ); + case HomeDisplaySection.forecast: + return ( + icon: Symbols.partly_cloudy_day_rounded, + color: Colors.orange, + title: '天氣預報(24h)'.i18n, + subtitle: '顯示未來 24 小時天氣預報'.i18n, + ); + case HomeDisplaySection.history: + return ( + icon: Symbols.history_rounded, + color: Colors.green, + title: '歷史事件'.i18n, + subtitle: '顯示地震與災害歷史紀錄'.i18n, + ); + case HomeDisplaySection.wind: + return ( + icon: Symbols.wind_power_rounded, + color: Colors.purple, + title: '風向'.i18n, + subtitle: '顯示風向與風力級數'.i18n, + ); + case HomeDisplaySection.community: + return ( + icon: Symbols.people_alt_rounded, + color: Colors.teal, + title: '社群動態'.i18n, + subtitle: '顯示來自社群的即時資訊'.i18n, + ); + } + } + Widget _buildHeader(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -138,7 +180,7 @@ class SettingsLayoutPage extends StatelessWidget { ), ), Text( - '自訂首頁顯示的區塊'.i18n, + '長按可拖曳排序顯示順序'.i18n, style: context.texts.bodySmall?.copyWith( color: context.colors.onSurfaceVariant, ), @@ -155,14 +197,17 @@ class SettingsLayoutPage extends StatelessWidget { Widget _buildSectionCard( BuildContext context, { + required Key key, required IconData icon, required Color iconColor, required String title, required String subtitle, required bool value, - required ValueChanged onChanged, + required ValueChanged? onChanged, + required bool isReorderable, }) { return Container( + key: key, margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), decoration: BoxDecoration( color: context.colors.surfaceContainerLow, @@ -172,11 +217,20 @@ class SettingsLayoutPage extends StatelessWidget { color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(16), - onTap: () => onChanged(!value), + onTap: onChanged == null ? null : () => onChanged(!value), child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ + if (isReorderable) ...[ + Icon( + Symbols.drag_handle_rounded, + color: context.colors.onSurfaceVariant.withValues( + alpha: 0.5, + ), + ), + const SizedBox(width: 12), + ], Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( diff --git a/lib/models/settings/ui.dart b/lib/models/settings/ui.dart index f6c39862a..3600bfb0b 100644 --- a/lib/models/settings/ui.dart +++ b/lib/models/settings/ui.dart @@ -10,8 +10,9 @@ import 'package:provider/provider.dart'; enum HomeDisplaySection { radar, forecast, - history, wind, + history, + community, } class SettingsUserInterfaceModel extends ChangeNotifier { @@ -21,7 +22,7 @@ class SettingsUserInterfaceModel extends ChangeNotifier { int? get _themeColor => Preference.themeColor; Locale? get _locale => Preference.locale?.asLocale; bool get _useFahrenheit => Preference.useFahrenheit ?? false; - late Set homeSections; + late List homeSections; final savedList = Preference.homeDisplaySections; ThemeMode get themeMode => ThemeMode.values.byName(_themeMode); @@ -64,7 +65,7 @@ class SettingsUserInterfaceModel extends ChangeNotifier { SettingsUserInterfaceModel() { if (savedList.isEmpty) { // 預設全部啟用 - homeSections = HomeDisplaySection.values.toSet(); + homeSections = HomeDisplaySection.values.toList(); } else { final saved = savedList .map( @@ -73,7 +74,7 @@ class SettingsUserInterfaceModel extends ChangeNotifier { .firstWhere((e) => e?.name == s, orElse: () => null), ) .whereType() - .toSet(); + .toList(); homeSections = saved; } } @@ -81,11 +82,28 @@ class SettingsUserInterfaceModel extends ChangeNotifier { bool isEnabled(HomeDisplaySection section) => homeSections.contains(section); void toggleSection(HomeDisplaySection section, bool enabled) { + final newList = List.from(homeSections); if (enabled) { - homeSections.add(section); + if (!newList.contains(section)) { + newList.add(section); + } } else { - homeSections.remove(section); + newList.remove(section); + } + homeSections = newList; + Preference.homeDisplaySections = homeSections.map((e) => e.name).toList(); + notifyListeners(); + } + + void reorderSection(int oldIndex, int newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; } + final newList = List.from(homeSections); + final item = newList.removeAt(oldIndex); + newList.insert(newIndex, item); + homeSections = newList; + Preference.homeDisplaySections = homeSections.map((e) => e.name).toList(); notifyListeners(); }