diff --git a/lib/app.dart b/lib/app.dart index 56f4010..afda4f0 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -186,15 +186,19 @@ class _Shell extends ConsumerStatefulWidget { } class _ShellState extends ConsumerState<_Shell> with WidgetsBindingObserver { - static const _tabs = [ + static const _baseTabs = [ Routes.briefing, Routes.knowledge, Routes.conversations, Routes.projects, - Routes.news, - Routes.calendar, ]; + List _tabs(bool rssEnabled) => [ + ..._baseTabs, + if (rssEnabled) Routes.news, + Routes.calendar, + ]; + // Minimum gap between app-resume refreshes to avoid hammering the server. static const _resumeCooldown = Duration(seconds: 30); DateTime? _lastResumeRefresh; @@ -251,19 +255,18 @@ class _ShellState extends ConsumerState<_Shell> with WidgetsBindingObserver { ref.invalidate(briefingProvider); } - /// Refresh only the provider backing the given shell tab index. - void _refreshTab(int index) { - switch (index) { - case 0: - ref.invalidate(briefingProvider); - case 1: - ref.read(knowledgeProvider.notifier).refresh(); - case 2: - ref.read(conversationsProvider.notifier).refresh(); - case 4: - ref.read(newsProvider.notifier).refresh(); - case 5: - ref.read(calendarProvider.notifier).refresh(); + /// Refresh only the provider backing the given shell tab route. + void _refreshTab(String route) { + if (route == Routes.briefing) { + ref.invalidate(briefingProvider); + } else if (route == Routes.knowledge) { + ref.read(knowledgeProvider.notifier).refresh(); + } else if (route == Routes.conversations) { + ref.read(conversationsProvider.notifier).refresh(); + } else if (route == Routes.news) { + ref.read(newsProvider.notifier).refresh(); + } else if (route == Routes.calendar) { + ref.read(calendarProvider.notifier).refresh(); } } @@ -276,9 +279,9 @@ class _ShellState extends ConsumerState<_Shell> with WidgetsBindingObserver { } } - int _tabIndex(String location) { - for (var i = 0; i < _tabs.length; i++) { - if (location.startsWith(_tabs[i])) return i; + int _tabIndex(String location, List tabs) { + for (var i = 0; i < tabs.length; i++) { + if (location.startsWith(tabs[i])) return i; } return 0; } @@ -298,14 +301,15 @@ class _ShellState extends ConsumerState<_Shell> with WidgetsBindingObserver { context.push(Routes.projects); }, ), - ListTile( - leading: const Icon(Icons.newspaper_outlined), - title: const Text('News'), - onTap: () { - Navigator.pop(context); - context.push(Routes.news); - }, - ), + if (ref.read(rssEnabledProvider)) + ListTile( + leading: const Icon(Icons.newspaper_outlined), + title: const Text('News'), + onTap: () { + Navigator.pop(context); + context.push(Routes.news); + }, + ), ListTile( leading: const Icon(Icons.calendar_month_outlined), title: const Text('Calendar'), @@ -343,12 +347,15 @@ class _ShellState extends ConsumerState<_Shell> with WidgetsBindingObserver { .addPostFrameCallback((_) => _showUpdateSnackbar(next)); } }); + final rssEnabled = ref.watch(rssEnabledProvider); + final tabs = _tabs(rssEnabled); final location = GoRouterState.of(context).matchedLocation; - final index = _tabIndex(location); + final index = _tabIndex(location, tabs); // Refresh the incoming tab's data when switching between shell tabs. if (_prevTabIndex != null && _prevTabIndex != index) { - WidgetsBinding.instance.addPostFrameCallback((_) => _refreshTab(index)); + final route = index < tabs.length ? tabs[index] : tabs[0]; + WidgetsBinding.instance.addPostFrameCallback((_) => _refreshTab(route)); } _prevTabIndex = index; @@ -366,35 +373,36 @@ class _ShellState extends ConsumerState<_Shell> with WidgetsBindingObserver { children: [ NavigationRail( selectedIndex: index, - onDestinationSelected: (i) => context.go(_tabs[i]), + onDestinationSelected: (i) => context.go(tabs[i]), labelType: NavigationRailLabelType.all, - destinations: const [ - NavigationRailDestination( + destinations: [ + const NavigationRailDestination( icon: Icon(Icons.wb_sunny_outlined), selectedIcon: Icon(Icons.wb_sunny), label: Text('Briefing'), ), - NavigationRailDestination( + const NavigationRailDestination( icon: Icon(Icons.menu_book_outlined), selectedIcon: Icon(Icons.menu_book), label: Text('Knowledge'), ), - NavigationRailDestination( + const NavigationRailDestination( icon: Icon(Icons.chat_bubble_outline), selectedIcon: Icon(Icons.chat_bubble), label: Text('Chat'), ), - NavigationRailDestination( + const NavigationRailDestination( icon: Icon(Icons.folder_outlined), selectedIcon: Icon(Icons.folder), label: Text('Projects'), ), - NavigationRailDestination( - icon: Icon(Icons.newspaper_outlined), - selectedIcon: Icon(Icons.newspaper), - label: Text('News'), - ), - NavigationRailDestination( + if (ref.watch(rssEnabledProvider)) + const NavigationRailDestination( + icon: Icon(Icons.newspaper_outlined), + selectedIcon: Icon(Icons.newspaper), + label: Text('News'), + ), + const NavigationRailDestination( icon: Icon(Icons.calendar_month_outlined), selectedIcon: Icon(Icons.calendar_month), label: Text('Calendar'), @@ -431,7 +439,7 @@ class _ShellState extends ConsumerState<_Shell> with WidgetsBindingObserver { if (i == 3) { _showMoreSheet(context); } else { - context.go(_tabs[i]); + context.go(tabs[i]); } }, destinations: const [ diff --git a/lib/data/api/settings_api.dart b/lib/data/api/settings_api.dart index 8d30c5f..3042786 100644 --- a/lib/data/api/settings_api.dart +++ b/lib/data/api/settings_api.dart @@ -10,4 +10,13 @@ class SettingsApi { data: {'user_timezone': ianaTimezone}, ); } + + Future> getAll() async { + final response = await _dio.get>('/api/settings'); + return response.data ?? {}; + } + + Future update(Map updates) async { + await _dio.put('/api/settings', data: updates); + } } diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 38478b4..62a6682 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'api_client_provider.dart'; + const _kServerUrl = 'server_url'; const _kThemeMode = 'theme_mode'; const _kForgejoRepoUrl = 'forgejo_repo_url'; @@ -82,3 +84,32 @@ class ServerUrlNotifier extends Notifier { state = null; } } + +final serverSettingsProvider = + AsyncNotifierProvider>( + ServerSettingsNotifier.new); + +class ServerSettingsNotifier extends AsyncNotifier> { + @override + Future> build() async { + try { + return await ref.read(settingsApiProvider).getAll(); + } catch (_) { + return {}; + } + } + + bool get rssEnabled { + final data = state.value ?? {}; + return data['rss_enabled']?.toString().toLowerCase() == 'true'; + } + + Future refresh() async { + state = AsyncData(await ref.read(settingsApiProvider).getAll()); + } +} + +final rssEnabledProvider = Provider((ref) { + final settings = ref.watch(serverSettingsProvider).value ?? {}; + return settings['rss_enabled']?.toString().toLowerCase() == 'true'; +}); diff --git a/lib/screens/briefing/briefing_screen.dart b/lib/screens/briefing/briefing_screen.dart index 41678d0..90fcfb0 100644 --- a/lib/screens/briefing/briefing_screen.dart +++ b/lib/screens/briefing/briefing_screen.dart @@ -11,6 +11,7 @@ import '../../widgets/chat_message_bubble.dart'; import '../../widgets/weather_card.dart'; import '../../widgets/news_card.dart'; import 'briefing_history_screen.dart'; +import '../../providers/settings_provider.dart'; import '../../providers/voice_provider.dart'; import '../../widgets/voice_mic_button.dart'; @@ -313,6 +314,7 @@ class _BriefingScreenState extends ConsumerState reactions: _reactions, onReaction: _handleReaction, onDiscuss: _handleDiscuss, + rssEnabled: ref.watch(rssEnabledProvider), ); }, ), @@ -428,6 +430,7 @@ class _BriefingMessageItem extends StatelessWidget { final Map reactions; final void Function(int itemId, String reaction) onReaction; final void Function(int convId, int itemId) onDiscuss; + final bool rssEnabled; const _BriefingMessageItem({ required this.message, @@ -435,6 +438,7 @@ class _BriefingMessageItem extends StatelessWidget { required this.reactions, required this.onReaction, required this.onDiscuss, + this.rssEnabled = false, }); @override @@ -446,11 +450,12 @@ class _BriefingMessageItem extends StatelessWidget { final bool hasWeatherKey = isAssistant && meta != null && meta.containsKey('weather'); final weatherData = hasWeatherKey ? meta['weather'] as Map? : null; - // RSS news cards — cap at 3 - final rssItemsRaw = isAssistant && meta != null - ? (meta['rss_items'] as List?)?.cast>() ?? [] - : >[]; - final rssItems = rssItemsRaw.map(RssItemMeta.fromJson).take(3).toList(); + // RSS news cards — cap at 3 (only when RSS is enabled) + final rssItems = []; + if (rssEnabled && isAssistant && meta != null) { + final raw = (meta['rss_items'] as List?)?.cast>() ?? []; + rssItems.addAll(raw.map(RssItemMeta.fromJson).take(3)); + } return Column( crossAxisAlignment: CrossAxisAlignment.stretch,