import 'package:flutter/material.dart'; import 'package:flutter_fullscreen/flutter_fullscreen.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:sheetless/core/models/config.dart'; import 'package:sheetless/core/models/sheet.dart'; import 'package:sheetless/core/services/api_client.dart'; import 'package:sheetless/core/services/storage_service.dart'; import 'package:sheetless/core/services/sync_service.dart'; import '../../app.dart'; import '../auth/login_page.dart'; import '../sheet_viewer/sheet_viewer_page.dart'; import 'widgets/app_drawer.dart'; import 'widgets/sheets_list.dart'; /// Main home page displaying the list of sheet music. /// /// Features: /// - Pull-to-refresh sheet list /// - Shuffle mode for random practice /// - Navigation to sheet viewer /// - Logout functionality class HomePage extends StatefulWidget { final Config config; const HomePage({super.key, required this.config}); @override State createState() => _HomePageState(); } class _HomePageState extends State with RouteAware { final _log = Logger('HomePage'); final _storageService = StorageService(); ApiClient? _apiClient; SyncService? _syncService; late Future _syncFuture; List _sheets = []; bool _isShuffling = false; bool _isOnline = true; String? _appName; String? _appVersion; @override void initState() { super.initState(); // Exit fullscreen when entering home page if (FullScreen.isFullScreen) { FullScreen.setFullScreen(false); } // Subscribe to route changes WidgetsBinding.instance.addPostFrameCallback((_) { routeObserver.subscribe(this, ModalRoute.of(context)!); }); _loadAppInfo(); _syncFuture = _loadSheets(); } @override void dispose() { routeObserver.unsubscribe(this); super.dispose(); } // --------------------------------------------------------------------------- // Route Aware (Fullscreen Management) // --------------------------------------------------------------------------- @override void didPush() { // Exit fullscreen when entering home page if (FullScreen.isFullScreen) { FullScreen.setFullScreen(false); } super.didPush(); } @override void didPopNext() { // Exit fullscreen when returning to home page if (FullScreen.isFullScreen) { FullScreen.setFullScreen(false); } super.didPopNext(); } // --------------------------------------------------------------------------- // Data Loading // --------------------------------------------------------------------------- Future _loadAppInfo() async { final info = await PackageInfo.fromPlatform(); setState(() { _appName = info.appName; _appVersion = info.version; }); } Future _loadSheets() async { final url = await _storageService.readSecure(SecureStorageKey.url); final jwt = await _storageService.readSecure(SecureStorageKey.jwt); _apiClient = ApiClient(baseUrl: url!, token: jwt); _syncService = SyncService( apiClient: _apiClient!, storageService: _storageService, ); // Perform sync (fetches sheets, uploads pending changes/annotations) final result = await _syncService!.sync(); _log.info( '${result.sheets.length} sheets loaded (online: ${result.isOnline}, ' 'changes synced: ${result.changesSynced}, ' 'annotations synced: ${result.annotationsSynced})', ); // Sort and store sheets _sheets = await _sortSheetsByRecency(result.sheets); _isOnline = result.isOnline; return result; } Future> _sortSheetsByRecency(List sheets) async { final accessTimes = await _storageService.readSheetAccessTimes(); sheets.sort((a, b) { // Use local access time if available and more recent than server update var dateA = accessTimes[a.uuid]; var dateB = accessTimes[b.uuid]; if (dateA == null || a.updatedAt.isAfter(dateA)) { dateA = a.updatedAt; } if (dateB == null || b.updatedAt.isAfter(dateB)) { dateB = b.updatedAt; } return dateB.compareTo(dateA); // Most recent first }); return sheets; } Future _refreshSheets() async { setState(() { _syncFuture = _loadSheets(); }); } // --------------------------------------------------------------------------- // Actions // --------------------------------------------------------------------------- void _handleShuffleChanged(bool enabled) async { if (enabled) { _sheets.shuffle(); } else { await _sortSheetsByRecency(_sheets); } setState(() => _isShuffling = enabled); } Future _handleLogout() async { await _storageService.clearAllUserData(); if (!mounted) return; Navigator.of( context, ).pushReplacement(MaterialPageRoute(builder: (_) => const LoginPage())); } void _openSheet(Sheet sheet) { // Record access time for recency sorting _storageService.writeSheetAccessTime(sheet.uuid, DateTime.now()); Navigator.push( context, MaterialPageRoute( builder: (_) => SheetViewerPage( sheet: sheet, apiClient: _apiClient!, config: widget.config, ), ), ); } // --------------------------------------------------------------------------- // UI // --------------------------------------------------------------------------- @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Row( mainAxisSize: MainAxisSize.min, children: [ const Text('Sheetless'), if (!_isOnline) const Padding( padding: EdgeInsets.only(left: 8), child: Icon(Icons.cloud_off, color: Colors.orange, size: 20), ), ], ), ), endDrawer: AppDrawer( isShuffling: _isShuffling, onShuffleChanged: _handleShuffleChanged, onLogout: _handleLogout, appName: _appName, appVersion: _appVersion, syncFuture: _syncFuture, ), body: RefreshIndicator(onRefresh: _refreshSheets, child: _buildBody()), ); } Widget _buildBody() { return FutureBuilder( future: _syncFuture, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { _log.warning('Error loading sheets', snapshot.error); return _buildError(snapshot.error.toString()); } if (snapshot.hasData) { return SheetsList( sheets: _sheets, onSheetSelected: _openSheet, syncService: _syncService!, ); } return const Center(child: CircularProgressIndicator()); }, ); } Widget _buildError(String message) { return Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Text( message, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(color: Colors.red), textAlign: TextAlign.center, ), ), ); } }