Save and sort sheets by last access time
This commit is contained in:
@@ -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());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user