Save and sort sheets by last access time

This commit is contained in:
2025-09-13 20:11:22 +02:00
parent a0d6368f02
commit 89aa15b8b8
7 changed files with 141 additions and 70 deletions

View File

@@ -25,50 +25,75 @@ class _MyHomePageState extends State<MyHomePage> {
} }
Future<List<Sheet>> acquireSheets() async { Future<List<Sheet>> acquireSheets() async {
final url = await _storageHelper.read(StorageKey.url); final url = await _storageHelper.readSecure(SecureStorageKey.url);
final jwt = await _storageHelper.read(StorageKey.jwt); final jwt = await _storageHelper.readSecure(SecureStorageKey.jwt);
apiClient = ApiClient(baseUrl: url!, token: jwt); apiClient = ApiClient(baseUrl: url!, token: jwt);
// TODO: check if really logged in // TODO: check if really logged in
final sheets = await apiClient!.fetchSheets(); final sheets = await apiClient!.fetchSheets();
log.info("${sheets.length} sheets fetched"); log.info("${sheets.length} sheets fetched");
final sheetsSorted = await sortSheetsByAccessTime(sheets);
log.info("${sheetsSorted.length} sheets sorted");
return sheetsSorted;
}
Future<List<Sheet>> sortSheetsByAccessTime(List<Sheet> 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; return sheets;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(title: const Text("My Sheets")),
title: const Text("My Sheets"),
),
body: FutureBuilder( body: FutureBuilder(
future: acquireSheets(), future: acquireSheets(),
builder: (BuildContext context, AsyncSnapshot<List<Sheet>> snapshot) { builder: (BuildContext context, AsyncSnapshot<List<Sheet>> snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
return SheetsWidget( return SheetsWidget(
sheets: snapshot.data!, sheets: snapshot.data!,
callback: (sheet) => Navigator.push( onSheetOpenRequest: (sheet) {
_storageHelper.writeSheetAccessTime(sheet.uuid, DateTime.now());
Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => SheetViewerPage( builder: (context) =>
sheet: sheet, SheetViewerPage(sheet: sheet, apiClient: apiClient!),
apiClient: apiClient!,
),
), ),
), );
); },
} else if (snapshot.hasError) { );
return Center( } else if (snapshot.hasError) {
child: Text( return Center(
style: Theme.of(context) child: Text(
.textTheme style: Theme.of(
.displaySmall! context,
.copyWith(color: Colors.red), ).textTheme.displaySmall!.copyWith(color: Colors.red),
textAlign: TextAlign.center, textAlign: TextAlign.center,
snapshot.error.toString())); snapshot.error.toString(),
} else { ),
return const Center(child: CircularProgressIndicator()); );
} } else {
}), return const Center(child: CircularProgressIndicator());
}
},
),
); );
} }
} }

View File

@@ -26,16 +26,18 @@ class _LoginPageState extends State<LoginPage> {
} }
Future<void> _checkJwtValidity() async { Future<void> _checkJwtValidity() async {
final jwt = await _storageHelper.read(StorageKey.jwt); final jwt = await _storageHelper.readSecure(SecureStorageKey.jwt);
if (jwt != null) { if (jwt != null) {
final isValid = await _validateJwt(jwt); final isValid = await _validateJwt(jwt);
if (isValid) { if (isValid) {
_navigateToMainPage(); _navigateToMainPage();
return; return;
} else { } else {
final url = await _storageHelper.read(StorageKey.url); final url = await _storageHelper.readSecure(SecureStorageKey.url);
final email = await _storageHelper.read(StorageKey.email); final email = await _storageHelper.readSecure(SecureStorageKey.email);
final password = await _storageHelper.read(StorageKey.password); final password = await _storageHelper.readSecure(
SecureStorageKey.password,
);
if (url != null && email != null && password != null) { if (url != null && email != null && password != null) {
_login(url, email, password); _login(url, email, password);
} }
@@ -60,10 +62,10 @@ class _LoginPageState extends State<LoginPage> {
final apiClient = ApiClient(baseUrl: serverUrl); final apiClient = ApiClient(baseUrl: serverUrl);
final loginSuccessful = await apiClient.login(email, password); final loginSuccessful = await apiClient.login(email, password);
if (loginSuccessful) { if (loginSuccessful) {
await _storageHelper.write(StorageKey.url, serverUrl); await _storageHelper.writeSecure(SecureStorageKey.url, serverUrl);
await _storageHelper.write(StorageKey.jwt, apiClient.token!); await _storageHelper.writeSecure(SecureStorageKey.jwt, apiClient.token!);
await _storageHelper.write(StorageKey.email, email); await _storageHelper.writeSecure(SecureStorageKey.email, email);
await _storageHelper.write(StorageKey.password, password); await _storageHelper.writeSecure(SecureStorageKey.password, password);
_navigateToMainPage(); _navigateToMainPage();
} else { } else {
// TODO: give more varied error messages // TODO: give more varied error messages
@@ -74,9 +76,9 @@ class _LoginPageState extends State<LoginPage> {
} }
void _navigateToMainPage() { void _navigateToMainPage() {
Navigator.of(context).pushReplacement( Navigator.of(
MaterialPageRoute(builder: (_) => MyHomePage()), context,
); ).pushReplacement(MaterialPageRoute(builder: (_) => MyHomePage()));
} }
@override @override
@@ -109,8 +111,11 @@ class _LoginPageState extends State<LoginPage> {
SizedBox(height: 16), SizedBox(height: 16),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
_login(_urlController.text, _emailController.text, _login(
_passwordController.text); _urlController.text,
_emailController.text,
_passwordController.text,
);
}, },
child: Text('Login'), child: Text('Login'),
), ),

View File

@@ -1,10 +1,15 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pdfrx/pdfrx.dart'; import 'package:pdfrx/pdfrx.dart';
import 'login_page.dart'; import 'login_page.dart';
void main() { Future<void> main() async {
Logger.root.level = Level.ALL; // defaults to Level.INFO Logger.root.level = Level.ALL; // defaults to Level.INFO
Logger.root.onRecord.listen((record) { Logger.root.onRecord.listen((record) {
debugPrint('${record.level.name}: ${record.time}: ${record.message}'); 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()); runApp(const MyApp());
} }

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:sheetless/storage_helper.dart';
class Sheet { class Sheet {
final String uuid; final String uuid;
@@ -28,9 +29,13 @@ class Sheet {
class SheetsWidget extends StatefulWidget { class SheetsWidget extends StatefulWidget {
final List<Sheet> sheets; final List<Sheet> sheets;
final ValueSetter<Sheet> callback; final ValueSetter<Sheet> onSheetOpenRequest;
const SheetsWidget({super.key, required this.sheets, required this.callback}); const SheetsWidget({
super.key,
required this.sheets,
required this.onSheetOpenRequest,
});
@override @override
State<SheetsWidget> createState() => _SheetsWidgetState(); State<SheetsWidget> createState() => _SheetsWidgetState();
@@ -38,6 +43,7 @@ class SheetsWidget extends StatefulWidget {
class _SheetsWidgetState extends State<SheetsWidget> { class _SheetsWidgetState extends State<SheetsWidget> {
late List<Sheet> filteredSheets; late List<Sheet> filteredSheets;
final StorageHelper storageHelper = StorageHelper();
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
Timer? _debounce; Timer? _debounce;
@@ -69,16 +75,15 @@ class _SheetsWidgetState extends State<SheetsWidget> {
String query = _searchController.text.toLowerCase().trim(); String query = _searchController.text.toLowerCase().trim();
List<String> terms = query.split(RegExp(r'\s+')); // Split by whitespace List<String> terms = query.split(RegExp(r'\s+')); // Split by whitespace
filteredSheets = filteredSheets = widget.sheets.where((sheet) {
widget.sheets.where((sheet) { String name = sheet.name.toLowerCase();
String name = sheet.name.toLowerCase(); String composer = sheet.composerName.toLowerCase();
String composer = sheet.composerName.toLowerCase();
// Each term must be found in either the name or composer // Each term must be found in either the name or composer
return terms.every( return terms.every(
(term) => name.contains(term) || composer.contains(term), (term) => name.contains(term) || composer.contains(term),
); );
}).toList(); }).toList();
}); });
} }
@@ -97,13 +102,12 @@ class _SheetsWidgetState extends State<SheetsWidget> {
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Search...', hintText: 'Search...',
prefixIcon: const Icon(Icons.search), prefixIcon: const Icon(Icons.search),
suffixIcon: suffixIcon: _searchController.text.isNotEmpty
_searchController.text.isNotEmpty ? IconButton(
? IconButton( icon: const Icon(Icons.clear),
icon: const Icon(Icons.clear), onPressed: _clearSearch,
onPressed: _clearSearch, )
) : null,
: null,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(8.0),
), ),
@@ -118,7 +122,11 @@ class _SheetsWidgetState extends State<SheetsWidget> {
return ListTile( return ListTile(
title: Text(sheet.name), title: Text(sheet.name),
subtitle: Text(sheet.composerName), subtitle: Text(sheet.composerName),
onTap: () => widget.callback(sheet), onTap: () => setState(() {
widget.onSheetOpenRequest(sheet);
widget.sheets.remove(sheet);
widget.sheets.insert(0, sheet);
}),
); );
}, },
), ),

View File

@@ -1,22 +1,36 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 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 { class StorageHelper {
final sheetAccessTimesBox = "sheetAccessTimes";
late FlutterSecureStorage secureStorage; late FlutterSecureStorage secureStorage;
StorageHelper() { StorageHelper() {
AndroidOptions getAndroidOptions() => const AndroidOptions( AndroidOptions getAndroidOptions() =>
encryptedSharedPreferences: true, const AndroidOptions(encryptedSharedPreferences: true);
);
secureStorage = FlutterSecureStorage(aOptions: getAndroidOptions()); secureStorage = FlutterSecureStorage(aOptions: getAndroidOptions());
} }
Future<String?> read(StorageKey key) { Future<String?> readSecure(SecureStorageKey key) {
return secureStorage.read(key: key.name); return secureStorage.read(key: key.name);
} }
Future<void> write(StorageKey key, String value) { Future<void> writeSecure(SecureStorageKey key, String value) {
return secureStorage.write(key: key.name, value: value); return secureStorage.write(key: key.name, value: value);
} }
Future<Map<String, DateTime>> readSheetAccessTimes() async {
final box = await Hive.openBox(sheetAccessTimesBox);
return box.toMap().map(
(k, v) => MapEntry(k as String, DateTime.parse(v as String)),
);
}
Future<void> writeSheetAccessTime(String uuid, DateTime datetime) async {
final box = await Hive.openBox(sheetAccessTimesBox);
await box.put(uuid, datetime.toIso8601String());
}
} }

View File

@@ -224,6 +224,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
hive:
dependency: "direct main"
description:
name: hive
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
url: "https://pub.dev"
source: hosted
version: "2.2.3"
http: http:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -47,6 +47,7 @@ dependencies:
logging: ^1.3.0 logging: ^1.3.0
flutter_drawing_board: ^0.9.8 flutter_drawing_board: ^0.9.8
flutter_launcher_icons: ^0.14.4 flutter_launcher_icons: ^0.14.4
hive: ^2.2.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: