diff --git a/lib/home_page.dart b/lib/home_page.dart index d3c4ac8..0af5691 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -25,50 +25,75 @@ class _MyHomePageState extends State { } Future> acquireSheets() async { - final url = await _storageHelper.read(StorageKey.url); - final jwt = await _storageHelper.read(StorageKey.jwt); + final url = await _storageHelper.readSecure(SecureStorageKey.url); + final jwt = await _storageHelper.readSecure(SecureStorageKey.jwt); apiClient = ApiClient(baseUrl: url!, token: jwt); // TODO: check if really logged in final sheets = await apiClient!.fetchSheets(); log.info("${sheets.length} sheets fetched"); + final sheetsSorted = await sortSheetsByAccessTime(sheets); + log.info("${sheetsSorted.length} sheets sorted"); + return sheetsSorted; + } + + Future> sortSheetsByAccessTime(List sheets) async { + final accessTimes = await _storageHelper.readSheetAccessTimes(); + + sheets.sort((a, b) { + final dateA = accessTimes[a.uuid]; + final dateB = accessTimes[b.uuid]; + + if (dateB == null) { + // b has no date, sort below a + return -1; + } else if (dateA == null) { + // a has no date, sort below b + return 1; + } else { + // compare both and sort by date + return dateB.compareTo(dateA); + } + }); + return sheets; } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text("My Sheets"), - ), + appBar: AppBar(title: const Text("My Sheets")), body: FutureBuilder( - future: acquireSheets(), - builder: (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.hasData) { - return SheetsWidget( - sheets: snapshot.data!, - callback: (sheet) => Navigator.push( + future: acquireSheets(), + builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + return SheetsWidget( + sheets: snapshot.data!, + onSheetOpenRequest: (sheet) { + _storageHelper.writeSheetAccessTime(sheet.uuid, DateTime.now()); + Navigator.push( context, MaterialPageRoute( - builder: (context) => SheetViewerPage( - sheet: sheet, - apiClient: apiClient!, - ), + builder: (context) => + SheetViewerPage(sheet: sheet, apiClient: apiClient!), ), - ), - ); - } else if (snapshot.hasError) { - return Center( - child: Text( - style: Theme.of(context) - .textTheme - .displaySmall! - .copyWith(color: Colors.red), - textAlign: TextAlign.center, - snapshot.error.toString())); - } else { - return const Center(child: CircularProgressIndicator()); - } - }), + ); + }, + ); + } else if (snapshot.hasError) { + return Center( + child: Text( + style: Theme.of( + context, + ).textTheme.displaySmall!.copyWith(color: Colors.red), + textAlign: TextAlign.center, + snapshot.error.toString(), + ), + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), ); } } diff --git a/lib/login_page.dart b/lib/login_page.dart index 425dd78..9c4fdad 100644 --- a/lib/login_page.dart +++ b/lib/login_page.dart @@ -26,16 +26,18 @@ class _LoginPageState extends State { } Future _checkJwtValidity() async { - final jwt = await _storageHelper.read(StorageKey.jwt); + final jwt = await _storageHelper.readSecure(SecureStorageKey.jwt); if (jwt != null) { final isValid = await _validateJwt(jwt); if (isValid) { _navigateToMainPage(); return; } else { - final url = await _storageHelper.read(StorageKey.url); - final email = await _storageHelper.read(StorageKey.email); - final password = await _storageHelper.read(StorageKey.password); + final url = await _storageHelper.readSecure(SecureStorageKey.url); + final email = await _storageHelper.readSecure(SecureStorageKey.email); + final password = await _storageHelper.readSecure( + SecureStorageKey.password, + ); if (url != null && email != null && password != null) { _login(url, email, password); } @@ -60,10 +62,10 @@ class _LoginPageState extends State { final apiClient = ApiClient(baseUrl: serverUrl); final loginSuccessful = await apiClient.login(email, password); if (loginSuccessful) { - await _storageHelper.write(StorageKey.url, serverUrl); - await _storageHelper.write(StorageKey.jwt, apiClient.token!); - await _storageHelper.write(StorageKey.email, email); - await _storageHelper.write(StorageKey.password, password); + await _storageHelper.writeSecure(SecureStorageKey.url, serverUrl); + await _storageHelper.writeSecure(SecureStorageKey.jwt, apiClient.token!); + await _storageHelper.writeSecure(SecureStorageKey.email, email); + await _storageHelper.writeSecure(SecureStorageKey.password, password); _navigateToMainPage(); } else { // TODO: give more varied error messages @@ -74,9 +76,9 @@ class _LoginPageState extends State { } void _navigateToMainPage() { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => MyHomePage()), - ); + Navigator.of( + context, + ).pushReplacement(MaterialPageRoute(builder: (_) => MyHomePage())); } @override @@ -109,8 +111,11 @@ class _LoginPageState extends State { SizedBox(height: 16), ElevatedButton( onPressed: () { - _login(_urlController.text, _emailController.text, - _passwordController.text); + _login( + _urlController.text, + _emailController.text, + _passwordController.text, + ); }, child: Text('Login'), ), diff --git a/lib/main.dart b/lib/main.dart index bf46759..34162b6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,15 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; import 'package:logging/logging.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:pdfrx/pdfrx.dart'; import 'login_page.dart'; -void main() { +Future main() async { Logger.root.level = Level.ALL; // defaults to Level.INFO Logger.root.onRecord.listen((record) { debugPrint('${record.level.name}: ${record.time}: ${record.message}'); @@ -13,7 +18,12 @@ void main() { } }); - pdfrxFlutterInitialize(); // Needed for web + pdfrxFlutterInitialize(); // Needed especially for web + + if (!kIsWeb) { + Directory dir = await getApplicationDocumentsDirectory(); + Hive.init(dir.path); // Needed only if not web + } runApp(const MyApp()); } diff --git a/lib/sheet.dart b/lib/sheet.dart index b0a9711..7d227ca 100644 --- a/lib/sheet.dart +++ b/lib/sheet.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:sheetless/storage_helper.dart'; class Sheet { final String uuid; @@ -28,9 +29,13 @@ class Sheet { class SheetsWidget extends StatefulWidget { final List sheets; - final ValueSetter callback; + final ValueSetter onSheetOpenRequest; - const SheetsWidget({super.key, required this.sheets, required this.callback}); + const SheetsWidget({ + super.key, + required this.sheets, + required this.onSheetOpenRequest, + }); @override State createState() => _SheetsWidgetState(); @@ -38,6 +43,7 @@ class SheetsWidget extends StatefulWidget { class _SheetsWidgetState extends State { late List filteredSheets; + final StorageHelper storageHelper = StorageHelper(); final TextEditingController _searchController = TextEditingController(); Timer? _debounce; @@ -69,16 +75,15 @@ class _SheetsWidgetState extends State { String query = _searchController.text.toLowerCase().trim(); List terms = query.split(RegExp(r'\s+')); // Split by whitespace - filteredSheets = - widget.sheets.where((sheet) { - String name = sheet.name.toLowerCase(); - String composer = sheet.composerName.toLowerCase(); + filteredSheets = widget.sheets.where((sheet) { + String name = sheet.name.toLowerCase(); + String composer = sheet.composerName.toLowerCase(); - // Each term must be found in either the name or composer - return terms.every( - (term) => name.contains(term) || composer.contains(term), - ); - }).toList(); + // Each term must be found in either the name or composer + return terms.every( + (term) => name.contains(term) || composer.contains(term), + ); + }).toList(); }); } @@ -97,13 +102,12 @@ class _SheetsWidgetState extends State { decoration: InputDecoration( hintText: 'Search...', prefixIcon: const Icon(Icons.search), - suffixIcon: - _searchController.text.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear), - onPressed: _clearSearch, - ) - : null, + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: _clearSearch, + ) + : null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8.0), ), @@ -118,7 +122,11 @@ class _SheetsWidgetState extends State { return ListTile( title: Text(sheet.name), subtitle: Text(sheet.composerName), - onTap: () => widget.callback(sheet), + onTap: () => setState(() { + widget.onSheetOpenRequest(sheet); + widget.sheets.remove(sheet); + widget.sheets.insert(0, sheet); + }), ); }, ), diff --git a/lib/storage_helper.dart b/lib/storage_helper.dart index 2704e21..4ea5a44 100644 --- a/lib/storage_helper.dart +++ b/lib/storage_helper.dart @@ -1,22 +1,36 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hive/hive.dart'; -enum StorageKey { url, jwt, email, password } +enum SecureStorageKey { url, jwt, email, password } class StorageHelper { + final sheetAccessTimesBox = "sheetAccessTimes"; + late FlutterSecureStorage secureStorage; StorageHelper() { - AndroidOptions getAndroidOptions() => const AndroidOptions( - encryptedSharedPreferences: true, - ); + AndroidOptions getAndroidOptions() => + const AndroidOptions(encryptedSharedPreferences: true); secureStorage = FlutterSecureStorage(aOptions: getAndroidOptions()); } - Future read(StorageKey key) { + Future readSecure(SecureStorageKey key) { return secureStorage.read(key: key.name); } - Future write(StorageKey key, String value) { + Future writeSecure(SecureStorageKey key, String value) { return secureStorage.write(key: key.name, value: value); } + + Future> readSheetAccessTimes() async { + final box = await Hive.openBox(sheetAccessTimesBox); + return box.toMap().map( + (k, v) => MapEntry(k as String, DateTime.parse(v as String)), + ); + } + + Future writeSheetAccessTime(String uuid, DateTime datetime) async { + final box = await Hive.openBox(sheetAccessTimesBox); + await box.put(uuid, datetime.toIso8601String()); + } } diff --git a/pubspec.lock b/pubspec.lock index 8da4bc7..7f108f3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -224,6 +224,14 @@ packages: description: flutter source: sdk version: "0.0.0" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" http: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 562719f..f4ef9a8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,7 @@ dependencies: logging: ^1.3.0 flutter_drawing_board: ^0.9.8 flutter_launcher_icons: ^0.14.4 + hive: ^2.2.3 dev_dependencies: flutter_test: