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 {
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<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;
}
@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<List<Sheet>> snapshot) {
if (snapshot.hasData) {
return SheetsWidget(
sheets: snapshot.data!,
callback: (sheet) => Navigator.push(
future: acquireSheets(),
builder: (BuildContext context, AsyncSnapshot<List<Sheet>> 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());
}
},
),
);
}
}

View File

@@ -26,16 +26,18 @@ class _LoginPageState extends State<LoginPage> {
}
Future<void> _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<LoginPage> {
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<LoginPage> {
}
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<LoginPage> {
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
_login(_urlController.text, _emailController.text,
_passwordController.text);
_login(
_urlController.text,
_emailController.text,
_passwordController.text,
);
},
child: Text('Login'),
),

View File

@@ -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<void> 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());
}

View File

@@ -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<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
State<SheetsWidget> createState() => _SheetsWidgetState();
@@ -38,6 +43,7 @@ class SheetsWidget extends StatefulWidget {
class _SheetsWidgetState extends State<SheetsWidget> {
late List<Sheet> filteredSheets;
final StorageHelper storageHelper = StorageHelper();
final TextEditingController _searchController = TextEditingController();
Timer? _debounce;
@@ -69,16 +75,15 @@ class _SheetsWidgetState extends State<SheetsWidget> {
String query = _searchController.text.toLowerCase().trim();
List<String> 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<SheetsWidget> {
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<SheetsWidget> {
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);
}),
);
},
),

View File

@@ -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<String?> read(StorageKey key) {
Future<String?> readSecure(SecureStorageKey key) {
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);
}
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());
}
}