From 41216ea43255cfc7e9d46fa8a7286ce1769a4de7 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Tue, 23 Sep 2025 00:58:58 +0530 Subject: [PATCH] refactor: optimize codebase --- lib/core/auth/auth_state_manager.dart | 20 +- lib/core/providers/app_providers.dart | 11 +- lib/core/providers/app_startup_providers.dart | 5 + lib/core/router/app_router.dart | 6 - lib/core/services/navigation_service.dart | 2 - .../persistent_streaming_service.dart | 49 +- lib/features/chat/views/chat_page.dart | 21 +- lib/features/files/views/workspace_page.dart | 445 ------------------ lib/main.dart | 2 +- lib/shared/widgets/optimized_list.dart | 13 +- 10 files changed, 80 insertions(+), 494 deletions(-) delete mode 100644 lib/features/files/views/workspace_page.dart diff --git a/lib/core/auth/auth_state_manager.dart b/lib/core/auth/auth_state_manager.dart index f442388..a40a0c5 100644 --- a/lib/core/auth/auth_state_manager.dart +++ b/lib/core/auth/auth_state_manager.dart @@ -384,7 +384,25 @@ class AuthStateManager extends Notifier { final username = savedCredentials['username']!; final password = savedCredentials['password']!; - // Set active server if needed + // Ensure the saved server still exists before switching + final serverConfigs = await ref.read(serverConfigsProvider.future); + final hasServer = serverConfigs.any((config) => config.id == serverId); + + if (!hasServer) { + await storage.deleteSavedCredentials(); + await storage.setActiveServerId(null); + ref.invalidate(serverConfigsProvider); + ref.invalidate(activeServerProvider); + + state = state.copyWith( + status: AuthStatus.error, + error: 'Saved server configuration is no longer available. Please reconnect.', + isLoading: false, + ); + return false; + } + + // Set active server once we know it exists await storage.setActiveServerId(serverId); ref.invalidate(activeServerProvider); diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index 5c2b6ad..c250e5d 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -126,10 +126,13 @@ final activeServerProvider = FutureProvider((ref) async { if (activeId == null || configs.isEmpty) return null; - return configs.firstWhere( - (config) => config.id == activeId, - orElse: () => configs.first, - ); + for (final config in configs) { + if (config.id == activeId) { + return config; + } + } + + return null; }); final serverConnectionStateProvider = Provider((ref) { diff --git a/lib/core/providers/app_startup_providers.dart b/lib/core/providers/app_startup_providers.dart index 99aee23..5089e61 100644 --- a/lib/core/providers/app_startup_providers.dart +++ b/lib/core/providers/app_startup_providers.dart @@ -8,6 +8,7 @@ import '../../features/auth/providers/unified_auth_providers.dart'; import '../services/navigation_service.dart'; import '../models/conversation.dart'; import '../services/background_streaming_handler.dart'; +import '../services/persistent_streaming_service.dart'; import '../../features/onboarding/views/onboarding_sheet.dart'; import '../../shared/theme/theme_extensions.dart'; import '../services/connectivity_service.dart'; @@ -124,6 +125,10 @@ final appStartupFlowProvider = Provider((ref) { // Keep Socket.IO connection alive in background within platform limits ref.watch(socketPersistenceProvider); + // Ensure persistent streaming uses the shared connectivity service + final connectivityService = ref.watch(connectivityServiceProvider); + PersistentStreamingService().attachConnectivityService(connectivityService); + // Warm the conversations list in the background as soon as possible Future.microtask(() => _scheduleConversationWarmup(ref)); diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index eb7b6d0..233fd2f 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -10,7 +10,6 @@ import '../../features/auth/views/authentication_page.dart'; import '../../features/auth/views/connect_signin_page.dart'; import '../../features/auth/views/server_connection_page.dart'; import '../../features/chat/views/chat_page.dart'; -import '../../features/files/views/workspace_page.dart'; import '../../features/navigation/views/splash_launcher_page.dart'; import '../../features/profile/views/app_customization_page.dart'; import '../../features/profile/views/profile_page.dart'; @@ -147,11 +146,6 @@ final goRouterProvider = Provider((ref) { name: RouteNames.appCustomization, builder: (context, state) => const AppCustomizationPage(), ), - GoRoute( - path: Routes.workspace, - name: RouteNames.workspace, - builder: (context, state) => const WorkspacePage(), - ), ]; final router = GoRouter( diff --git a/lib/core/services/navigation_service.dart b/lib/core/services/navigation_service.dart index 4f7b6c0..a43aa05 100644 --- a/lib/core/services/navigation_service.dart +++ b/lib/core/services/navigation_service.dart @@ -98,7 +98,6 @@ class Routes { static const String authentication = '/authentication'; static const String profile = '/profile'; static const String appCustomization = '/profile/customization'; - static const String workspace = '/workspace'; } /// Friendly names for GoRouter routes to support context.pushNamed. @@ -110,5 +109,4 @@ class RouteNames { static const String authentication = 'authentication'; static const String profile = 'profile'; static const String appCustomization = 'app-customization'; - static const String workspace = 'workspace'; } diff --git a/lib/core/services/persistent_streaming_service.dart b/lib/core/services/persistent_streaming_service.dart index 63250e9..11f716b 100644 --- a/lib/core/services/persistent_streaming_service.dart +++ b/lib/core/services/persistent_streaming_service.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; -import 'package:dio/dio.dart'; import 'background_streaming_handler.dart'; import 'connectivity_service.dart'; import '../utils/debug_logger.dart'; @@ -31,6 +30,7 @@ class PersistentStreamingService with WidgetsBindingObserver { // Connectivity monitoring StreamSubscription? _connectivitySubscription; + ConnectivityService? _connectivityService; bool _hasConnectivity = true; // Recovery state @@ -42,7 +42,6 @@ class PersistentStreamingService with WidgetsBindingObserver { WidgetsBinding.instance.addObserver(this); _backgroundHandler = BackgroundStreamingHandler.instance; _setupBackgroundHandlerCallbacks(); - _setupConnectivityMonitoring(); _startHeartbeat(); } @@ -68,31 +67,31 @@ class PersistentStreamingService with WidgetsBindingObserver { }; } - void _setupConnectivityMonitoring() { - // Create a connectivity service instance - this would normally be injected - // For now, create a temporary instance just for monitoring - final connectivityService = ConnectivityService(Dio()); + void attachConnectivityService(ConnectivityService service) { + if (identical(_connectivityService, service)) { + return; + } - _connectivitySubscription = connectivityService.isConnected.listen(( - connected, - ) { - final wasConnected = _hasConnectivity; - _hasConnectivity = connected; + _connectivitySubscription?.cancel(); + _connectivityService = service; + _connectivitySubscription = service.isConnected.listen(_handleConnectivityChange); + } - if (!wasConnected && connected) { - // Connectivity restored - try to recover streams - DebugLogger.stream( - 'PersistentStreaming: Connectivity restored, recovering streams', - ); - _recoverActiveStreams(); - } else if (wasConnected && !connected) { - // Connectivity lost - mark streams as suspended - DebugLogger.stream( - 'PersistentStreaming: Connectivity lost, suspending streams', - ); - _suspendAllStreams(); - } - }); + void _handleConnectivityChange(bool connected) { + final wasConnected = _hasConnectivity; + _hasConnectivity = connected; + + if (!wasConnected && connected) { + DebugLogger.stream( + 'PersistentStreaming: Connectivity restored, recovering streams', + ); + _recoverActiveStreams(); + } else if (wasConnected && !connected) { + DebugLogger.stream( + 'PersistentStreaming: Connectivity lost, suspending streams', + ); + _suspendAllStreams(); + } } void _startHeartbeat() { diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 8db38fb..aee45ed 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -1045,11 +1045,24 @@ class _ChatPageState extends ConsumerState { // Keep any local persistence only. if (context.mounted) { - final canPopNavigator = Navigator.of(context).canPop(); - if (canPopNavigator) { - Navigator.of(context).pop(); + final navigator = Navigator.of(context); + if (navigator.canPop()) { + navigator.pop(); } else { - SystemNavigator.pop(); + final shouldExit = await ThemedDialogs.confirm( + context, + title: l10n.appTitle, + message: l10n.endYourSession, + confirmText: l10n.confirm, + cancelText: l10n.cancel, + isDestructive: Platform.isAndroid, + ); + + if (!shouldExit || !context.mounted) return; + + if (Platform.isAndroid) { + SystemNavigator.pop(); + } } } }, diff --git a/lib/features/files/views/workspace_page.dart b/lib/features/files/views/workspace_page.dart deleted file mode 100644 index d2c8c9a..0000000 --- a/lib/features/files/views/workspace_page.dart +++ /dev/null @@ -1,445 +0,0 @@ -import 'package:flutter/material.dart'; -import '../../../shared/theme/theme_extensions.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import '../../../core/widgets/error_boundary.dart'; -import '../../../core/services/navigation_service.dart'; -import '../../../shared/widgets/improved_loading_states.dart'; - -import '../../../shared/utils/ui_utils.dart'; -import '../../../shared/widgets/sheet_handle.dart'; -import 'package:conduit/l10n/app_localizations.dart'; - -/// Files page for managing documents and uploads -class WorkspacePage extends ConsumerStatefulWidget { - const WorkspacePage({super.key}); - - @override - ConsumerState createState() => _WorkspacePageState(); -} - -class _WorkspacePageState extends ConsumerState - with TickerProviderStateMixin { - int _selectedTab = 0; - late AnimationController _tabAnimationController; - late AnimationController _contentAnimationController; - - @override - void initState() { - super.initState(); - _tabAnimationController = AnimationController( - duration: AnimationDuration.microInteraction, - vsync: this, - ); - _contentAnimationController = AnimationController( - duration: AnimationDuration.pageTransition, - vsync: this, - ); - } - - @override - void dispose() { - _tabAnimationController.dispose(); - _contentAnimationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ErrorBoundary( - child: Scaffold( - backgroundColor: context.conduitTheme.surfaceBackground, - appBar: _buildAppBar(), - body: Column( - children: [ - // Enhanced tab selector with animations - _buildTabSelector().animate().fadeIn( - duration: AnimationDuration.fast, - delay: AnimationDelay.short, - ), - - // Animated content - Expanded( - child: AnimatedSwitcher( - duration: AnimationDuration.pageTransition, - transitionBuilder: (Widget child, Animation animation) { - return FadeTransition( - opacity: animation, - child: SlideTransition( - position: - Tween( - begin: const Offset(0.05, 0), - end: Offset.zero, - ).animate( - CurvedAnimation( - parent: animation, - curve: AnimationCurves.pageTransition, - ), - ), - child: child, - ), - ); - }, - child: _selectedTab == 0 - ? _buildRecentFiles() - : _buildKnowledgeBase(), - ), - ), - ], - ), - ), - ); - } - - PreferredSizeWidget _buildAppBar() { - return AppBar( - backgroundColor: context.conduitTheme.surfaceBackground, - elevation: Elevation.none, - automaticallyImplyLeading: false, - toolbarHeight: TouchTarget.appBar, - titleSpacing: 0.0, - leading: IconButton( - icon: Icon( - UiUtils.platformIcon( - ios: CupertinoIcons.back, - android: Icons.arrow_back, - ), - color: context.conduitTheme.textPrimary, - size: IconSize.button, - ), - onPressed: () => NavigationService.goBack(), - tooltip: AppLocalizations.of(context)!.back, - ), - title: Text( - AppLocalizations.of(context)!.workspace, - style: AppTypography.headlineSmallStyle.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - centerTitle: true, - actions: [ - // Enhanced upload button with proper touch target - Container( - width: TouchTarget.iconButton, - height: TouchTarget.iconButton, - margin: const EdgeInsets.only(right: Spacing.screenPadding), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(AppBorderRadius.button), - onTap: _showUploadOptions, - child: Icon( - UiUtils.addIcon, - color: context.conduitTheme.iconPrimary, - size: IconSize.button, - ), - ), - ), - ), - ], - ); - } - - Widget _buildTabSelector() { - return Container( - margin: const EdgeInsets.symmetric( - horizontal: Spacing.pagePadding, - vertical: Spacing.sm, - ), - padding: const EdgeInsets.all(Spacing.xs), - decoration: BoxDecoration( - color: context.conduitTheme.surfaceContainer, - borderRadius: BorderRadius.circular(AppBorderRadius.card), - border: Border.all( - color: context.conduitTheme.cardBorder, - width: BorderWidth.thin, - ), - boxShadow: ConduitShadows.card, - ), - child: Row( - children: [ - Expanded( - child: _buildTabButton( - index: 0, - label: AppLocalizations.of(context)!.recentFiles, - isSelected: _selectedTab == 0, - ), - ), - const SizedBox(width: Spacing.xs), - Expanded( - child: _buildTabButton( - index: 1, - label: AppLocalizations.of(context)!.knowledgeBase, - isSelected: _selectedTab == 1, - ), - ), - ], - ), - ); - } - - Widget _buildTabButton({ - required int index, - required String label, - required bool isSelected, - }) { - return GestureDetector( - onTap: () { - setState(() => _selectedTab = index); - _tabAnimationController.forward(from: 0); - _contentAnimationController.forward(from: 0); - }, - child: AnimatedContainer( - duration: AnimationDuration.microInteraction, - curve: AnimationCurves.buttonPress, - padding: const EdgeInsets.symmetric( - vertical: Spacing.buttonPadding, - horizontal: Spacing.md, - ), - decoration: BoxDecoration( - color: isSelected - ? context.conduitTheme.buttonPrimary - : context.conduitTheme.surfaceBackground.withValues( - alpha: Alpha.hover, - ), - borderRadius: BorderRadius.circular(AppBorderRadius.button), - boxShadow: isSelected ? ConduitShadows.button : null, - ), - child: Text( - label, - style: context.conduitTheme.label?.copyWith( - color: isSelected - ? context.conduitTheme.textInverse - : context.conduitTheme.textSecondary, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, - ), - textAlign: TextAlign.center, - ), - ), - ); - } - - Widget _buildRecentFiles() { - return Container( - key: const ValueKey('recent_files'), - padding: const EdgeInsets.all(Spacing.pagePadding), - child: ImprovedEmptyState( - icon: UiUtils.platformIcon( - ios: CupertinoIcons.doc, - android: Icons.description_outlined, - ), - title: AppLocalizations.of(context)!.noFilesYet, - subtitle: AppLocalizations.of(context)!.uploadDocsPrompt, - onAction: _showUploadOptions, - actionLabel: AppLocalizations.of(context)!.uploadFirstFile, - showAnimation: true, - ), - ).animate().fadeIn( - duration: AnimationDuration.messageAppear, - delay: AnimationDelay.short, - ); - } - - Widget _buildKnowledgeBase() { - return Container( - key: const ValueKey('knowledge_base'), - padding: const EdgeInsets.all(Spacing.pagePadding), - child: ImprovedEmptyState( - icon: UiUtils.platformIcon( - ios: CupertinoIcons.book, - android: Icons.library_books, - ), - title: AppLocalizations.of(context)!.knowledgeBaseEmpty, - subtitle: AppLocalizations.of(context)!.createCollectionsPrompt, - onAction: _showKnowledgeBaseOptions, - actionLabel: 'Create knowledge base', - showAnimation: true, - ), - ).animate().fadeIn( - duration: AnimationDuration.messageAppear, - delay: AnimationDelay.short, - ); - } - - void _showUploadOptions() { - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - isScrollControlled: true, - builder: (context) => _buildUploadModal(), - ); - } - - Widget _buildUploadModal() { - return Container( - decoration: BoxDecoration( - color: context.conduitTheme.surfaceBackground, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(AppBorderRadius.modal), - topRight: Radius.circular(AppBorderRadius.modal), - ), - boxShadow: ConduitShadows.modal, - ), - child: SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Handle bar (standardized) - const SheetHandle(), - - // Header with enhanced typography - Padding( - padding: const EdgeInsets.all(Spacing.modalPadding), - child: Text( - AppLocalizations.of(context)!.uploadFileTitle, - style: context.conduitTheme.headingSmall?.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - ), - - // Enhanced upload options - _buildUploadOption( - icon: UiUtils.platformIcon( - ios: CupertinoIcons.camera, - android: Icons.camera_alt, - ), - title: AppLocalizations.of(context)!.takePhoto, - subtitle: AppLocalizations.of(context)!.captureDocumentOrImage, - onTap: () => _handleUploadOption('camera'), - ), - _buildUploadOption( - icon: UiUtils.platformIcon( - ios: CupertinoIcons.photo, - android: Icons.photo_library, - ), - title: AppLocalizations.of(context)!.chooseFromGallery, - subtitle: AppLocalizations.of(context)!.chooseFromGallery, - onTap: () => _handleUploadOption('gallery'), - ), - _buildUploadOption( - icon: UiUtils.platformIcon( - ios: CupertinoIcons.doc, - android: Icons.description, - ), - title: AppLocalizations.of(context)!.document, - subtitle: AppLocalizations.of(context)!.documentHint, - onTap: () => _handleUploadOption('document'), - ), - - const SizedBox(height: Spacing.modalPadding), - ], - ), - ), - ).animate().slide( - duration: AnimationDuration.modalPresentation, - curve: AnimationCurves.modalPresentation, - begin: const Offset(0, 1), - end: Offset.zero, - ); - } - - Widget _buildUploadOption({ - required IconData icon, - required String title, - required String subtitle, - required VoidCallback onTap, - }) { - return Container( - margin: const EdgeInsets.symmetric( - horizontal: Spacing.modalPadding, - vertical: Spacing.xs, - ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(AppBorderRadius.card), - onTap: onTap, - child: Container( - padding: const EdgeInsets.all(Spacing.listItemPadding), - decoration: BoxDecoration( - color: context.conduitTheme.surfaceContainer, - borderRadius: BorderRadius.circular(AppBorderRadius.card), - border: Border.all( - color: context.conduitTheme.cardBorder, - width: BorderWidth.thin, - ), - ), - child: Row( - children: [ - // Enhanced icon container - Container( - width: IconSize.avatar, - height: IconSize.avatar, - decoration: BoxDecoration( - color: context.conduitTheme.buttonPrimary.withValues( - alpha: Alpha.highlight, - ), - borderRadius: BorderRadius.circular(AppBorderRadius.avatar), - ), - child: Icon( - icon, - color: context.conduitTheme.buttonPrimary, - size: IconSize.medium, - ), - ), - const SizedBox(width: Spacing.md), - // Enhanced text content - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: context.conduitTheme.bodyLarge?.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: Spacing.xs), - Text( - subtitle, - style: context.conduitTheme.caption?.copyWith( - color: context.conduitTheme.textSecondary, - ), - ), - ], - ), - ), - Icon( - UiUtils.platformIcon( - ios: CupertinoIcons.chevron_right, - android: Icons.chevron_right, - ), - color: context.conduitTheme.iconSecondary, - size: IconSize.small, - ), - ], - ), - ), - ), - ), - ).animate().fadeIn( - duration: AnimationDuration.fast, - delay: AnimationDelay.staggeredDelay, - ); - } - - void _handleUploadOption(String type) { - NavigationService.goBack(); - UiUtils.showMessage( - context, - AppLocalizations.of(context)!.fileUploadComingSoon(type), - ); - } - - void _showKnowledgeBaseOptions() { - UiUtils.showMessage( - context, - AppLocalizations.of(context)!.kbCreationComingSoon, - ); - } -} diff --git a/lib/main.dart b/lib/main.dart index 2589d15..90f111f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -195,7 +195,7 @@ class _ConduitAppState extends ConsumerState { data: mediaQuery.copyWith( textScaler: mediaQuery.textScaler.clamp( minScaleFactor: 0.8, - maxScaleFactor: 1.3, + maxScaleFactor: 2.0, ), ), child: OfflineIndicator(child: child ?? const SizedBox.shrink()), diff --git a/lib/shared/widgets/optimized_list.dart b/lib/shared/widgets/optimized_list.dart index 06045cf..c680ec7 100644 --- a/lib/shared/widgets/optimized_list.dart +++ b/lib/shared/widgets/optimized_list.dart @@ -64,7 +64,6 @@ class OptimizedList extends ConsumerStatefulWidget { class _OptimizedListState extends ConsumerState> { late ScrollController _scrollController; bool _isLoadingMore = false; - final Set _visibleIndices = {}; @override void initState() { @@ -139,11 +138,16 @@ class _OptimizedListState extends ConsumerState> { // Build the list Widget listWidget; + final ScrollPhysics effectivePhysics = widget.physics ?? + (widget.onRefresh != null + ? const AlwaysScrollableScrollPhysics() + : const ClampingScrollPhysics()); + if (widget.separatorBuilder != null) { listWidget = ListView.separated( controller: _scrollController, padding: widget.padding, - physics: widget.physics ?? const AlwaysScrollableScrollPhysics(), + physics: effectivePhysics, keyboardDismissBehavior: widget.keyboardDismissBehavior, shrinkWrap: widget.shrinkWrap, scrollDirection: widget.scrollDirection, @@ -165,7 +169,7 @@ class _OptimizedListState extends ConsumerState> { listWidget = ListView.builder( controller: _scrollController, padding: widget.padding, - physics: widget.physics ?? const AlwaysScrollableScrollPhysics(), + physics: effectivePhysics, keyboardDismissBehavior: widget.keyboardDismissBehavior, shrinkWrap: widget.shrinkWrap, scrollDirection: widget.scrollDirection, @@ -196,9 +200,6 @@ class _OptimizedListState extends ConsumerState> { Widget _buildOptimizedItem(BuildContext context, int index) { final item = widget.items[index]; - // Track visible items for analytics - _visibleIndices.add(index); - // Wrap in repaint boundary for performance if (widget.addRepaintBoundaries) { return RepaintBoundary(child: widget.itemBuilder(context, item, index));