Work on implementing sheetable client

This commit is contained in:
2024-12-18 00:36:33 +01:00
parent 0825f048cd
commit f530a52e9d
8 changed files with 620 additions and 209 deletions

206
lib/api.dart Normal file
View File

@@ -0,0 +1,206 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart'; // For cache storage
import 'package:file/memory.dart';
import 'sheet.dart';
class ApiClient {
final String baseUrl =
'http://localhost:8080/api'; // Replace with your API base URL
String? _token; // Holds the JWT token after login
/// Checks if the user is authenticated
bool get isAuthenticated => _token != null;
/// Login and store the JWT token
Future<bool> login(String username, String password) async {
print("Logging in...");
try {
final url = '$baseUrl/login';
final response = await http.post(
Uri.parse(url),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'email': username,
'password': password,
}),
);
if (response.statusCode == 200) {
_token = jsonDecode(response.body);
print('Login successful, token: $_token');
return true;
} else {
print('Login failed: ${response.statusCode}, ${response.body}');
}
} catch (e) {
print('Error during login: $e');
}
return false;
}
/// Logout and clear the token
void logout() {
_token = null;
print('Logged out successfully.');
}
/// Make a GET request
Future<http.Response?> get(String endpoint, {bool isBinary = false}) async {
try {
final url = '$baseUrl$endpoint';
final headers = {
'Authorization': 'Bearer $_token',
if (!isBinary) 'Content-Type': 'application/json',
};
final response = await http.get(Uri.parse(url), headers: headers);
if (response.statusCode == 200) {
return response;
} else {
print('GET request failed: ${response.statusCode} ${response.body}');
}
} catch (e) {
print('Error during GET request: $e');
}
return null;
}
/// Make a POST request
Future<http.Response?> post(
String endpoint, Map<String, dynamic> body) async {
try {
final url = '$baseUrl$endpoint';
final headers = {
'Authorization': 'Bearer $_token',
'Content-Type': 'application/json',
};
final response = await http.post(
Uri.parse(url),
headers: headers,
body: jsonEncode(body),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return response;
} else {
print('POST request failed: ${response.statusCode} ${response.body}');
}
} catch (e) {
print('Error during POST request: $e');
}
return null;
}
/// Make a POST request with form data
Future<http.Response?> postFormData(String endpoint, String body) async {
try {
final url = '$baseUrl$endpoint';
final headers = {
'Authorization': 'Bearer $_token',
'Content-Type': 'application/x-www-form-urlencoded',
};
final response = await http.post(
Uri.parse(url),
headers: headers,
body: body,
);
if (response.statusCode == 200 || response.statusCode == 201) {
return response;
} else {
print(
'POST Form Data request failed: ${response.statusCode} ${response.body}');
}
} catch (e) {
print('Error during POST Form Data request: $e');
}
return null;
}
Future<List<Sheet>> fetchSheets({String sortBy = "last_opened desc"}) async {
try {
final bodyFormData = {
"page": 1,
"limit": "1000",
"sort_by": sortBy,
};
print("doing post...");
final response = await postFormData("/sheets", jsonEncode(bodyFormData));
print("got response...");
if (response == null) {
print("Empty reponse");
return List.empty();
}
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
print("Data: $data");
return (data['rows'] as List<dynamic>)
.map((sheet) => Sheet.fromJson(sheet as Map<String, dynamic>))
.toList();
} else {
print('Failed to fetch sheets with status: ${response.statusCode}');
print('Response: ${response.body}');
}
} catch (e) {
print('Error during fetching sheets: $e');
}
return List.empty();
}
Future<File?> getPdfFileCached(String sheetUuid) async {
try {
// Get the cache directory
print("Creating cache dir...");
// final cacheDir = kIsWeb
// ? await MemoryFileSystem().systemTempDirectory.createTemp('cache')
// : await getTemporaryDirectory();
final cacheDir = await getTemporaryDirectory();
final cachedPdfPath = '${cacheDir.path}/$sheetUuid.pdf';
print("cache dir created");
// Check if the file already exists in the cache
final cachedFile = File(cachedPdfPath);
print("file created: $cachedFile");
if (await cachedFile.exists()) {
print("PDF found in cache: $cachedPdfPath");
return cachedFile;
}
// Make the authenticated API call
print("getting response");
final response = await this.get('/sheet/pdf/$sheetUuid', isBinary: true);
print("got response");
if (response != null && response.statusCode == 200) {
// Save the fetched file to the cache
//
print("writing file...: $cachedFile");
await cachedFile.writeAsBytes(response.bodyBytes);
print("PDF downloaded and cached at: $cachedPdfPath");
return cachedFile;
} else {
print("Failed to fetch PDF from API. Status: ${response?.statusCode}");
}
} catch (e) {
print("Error fetching PDF: $e");
}
return null;
}
}

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:saf/saf.dart';
import 'package:sheetless/sheetview.dart';
import 'package:path_provider/path_provider.dart';
import 'api.dart';
import 'sheet.dart';
void main() {
@@ -33,38 +34,53 @@ class MyHomePage extends StatefulWidget {
}
class _MyHomePageState extends State<MyHomePage> {
ApiClient apiClient = ApiClient();
Future<bool> apiLoggedIn = Future.value(false);
@override
void initState() {
super.initState();
apiLoggedIn = apiClient.login("admin@admin.com", "sheetable");
}
Future<String?> getSafPickedSheetsDirectory() async {
await Permission.storage.request();
await Permission.manageExternalStorage.request();
var pickedDirectories = await Saf.getPersistedPermissionDirectories();
if (pickedDirectories == null || pickedDirectories.isEmpty) {
return null;
}
return pickedDirectories.last;
}
// Future<String?> getSafPickedSheetsDirectory() async {
// await Permission.storage.request();
// await Permission.manageExternalStorage.request();
// var pickedDirectories = await Saf.getPersistedPermissionDirectories();
// if (pickedDirectories == null || pickedDirectories.isEmpty) {
// return null;
// }
// return pickedDirectories.last;
// }
Future<List<Sheet>> acquireSheets() async {
String? sheetsDirectory = await getSafPickedSheetsDirectory();
if (sheetsDirectory == null || sheetsDirectory.isEmpty) {
await Saf.getDynamicDirectoryPermission(grantWritePermission: false);
sheetsDirectory = await getSafPickedSheetsDirectory();
if (sheetsDirectory == null || sheetsDirectory.isEmpty) {
throw Exception("No Directory selected");
}
}
// await Permission.storage.request();
// await Permission.manageExternalStorage.request();
//
var sheetsDirectoryFiles = await Saf.getFilesPathFor(sheetsDirectory);
if (sheetsDirectoryFiles == null) {
await Saf.releasePersistedPermissions();
throw Exception(
"Permissions for directory no longer valid or Directory deleted. Please restart app.");
}
return loadSheetsSorted(sheetsDirectoryFiles);
await apiLoggedIn; // TODO: check if really logged in (returns bool)
return await apiClient.fetchSheets();
// return api.main();
// final directory = await getApplicationDocumentsDirectory();
// print("Directory is: $directory");
// String? sheetsDirectory = "/home/julian/Klavier";
// if (sheetsDirectory == null || sheetsDirectory.isEmpty) {
// await Saf.getDynamicDirectoryPermission(grantWritePermission: false);
// sheetsDirectory = "/home/julian/Klavier";
// if (sheetsDirectory == null || sheetsDirectory.isEmpty) {
// throw Exception("No Directory selected");
// }
// }
// return List.empty();
// var sheetsDirectoryFiles = await Saf.getFilesPathFor(sheetsDirectory);
// if (sheetsDirectoryFiles == null) {
// await Saf.releasePersistedPermissions();
// throw Exception(
// "Permissions for directory no longer valid or Directory deleted. Please restart app.");
// }
// return loadSheetsSorted(sheetsDirectoryFiles);
}
@override
@@ -82,7 +98,10 @@ class _MyHomePageState extends State<MyHomePage> {
callback: (sheet) => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SheetViewerPage(sheet: sheet),
builder: (context) => SheetViewerPage(
sheet: sheet,
apiClient: apiClient,
),
),
),
);

View File

@@ -4,50 +4,53 @@ import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
class Sheet {
final String author;
final String uuid;
final String name;
final String path;
final String composerUuid;
final DateTime releaseDate;
final String file;
final String fileHash;
final bool wasUploaded;
final int uploaderId;
final DateTime createdAt;
final DateTime updatedAt;
final DateTime lastOpened;
final List<String> tags;
final String informationText;
Sheet(this.author, this.name, this.path);
}
Sheet({
required this.uuid,
required this.name,
required this.composerUuid,
required this.releaseDate,
required this.file,
required this.fileHash,
required this.wasUploaded,
required this.uploaderId,
required this.createdAt,
required this.updatedAt,
required this.lastOpened,
required this.tags,
required this.informationText,
});
Future<List<Sheet>> loadSheetsSorted(List<String> sheetsDirectoryFiles) async {
var sheets = await _loadSheets(sheetsDirectoryFiles);
sheets.sort((left, right) => left.name.compareTo(right.name));
return sheets;
}
Future<List<Sheet>> _loadSheets(List<String> sheetsDirectoryFiles) async {
final List<Sheet> sheets = List.empty(growable: true);
var authorDirectories = sheetsDirectoryFiles
.map((e) => Directory(e))
.where((element) => element.existsSync());
for (Directory authorDirectory in authorDirectories) {
var authorName = p.basename(authorDirectory.path);
// Ignore hidden directories
if (authorName.startsWith(".")) {
continue;
}
await for (final FileSystemEntity sheetFile in authorDirectory.list()) {
if (sheetFile is File) {
var sheetName = p.basenameWithoutExtension(sheetFile.path);
// Ignore hidden files
if (sheetName.startsWith(".")) {
continue;
}
sheetName = sheetName.capitalize();
sheets.add(Sheet(authorName, sheetName, sheetFile.path));
}
}
}
return sheets;
}
extension StringExtension on String {
String capitalize() {
return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
// Factory constructor for creating a Sheet from JSON
factory Sheet.fromJson(Map<String, dynamic> json) {
return Sheet(
uuid: json['uuid'],
name: json['sheet_name'],
composerUuid: json['composer_uuid'],
releaseDate: DateTime.parse(json['release_date']),
file: json['file'],
fileHash: json['file_hash'],
wasUploaded: json['was_uploaded'],
uploaderId: json['uploader_id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
lastOpened: DateTime.parse(json['last_opened']),
tags: List<String>.from(json['tags']),
informationText: json['information_text'],
);
}
}
@@ -65,7 +68,7 @@ class SheetsWidget extends StatelessWidget {
var sheet = sheets[index];
return ListTile(
title: Text(sheet.name),
subtitle: Text(sheet.author),
subtitle: Text(sheet.uuid),
onTap: () => callback(sheet),
);
});

View File

@@ -1,36 +1,63 @@
import 'package:flutter/material.dart';
import 'package:pdfx/pdfx.dart';
import 'package:sheetless/api.dart';
import 'package:sheetless/sheet.dart';
class SheetViewerPage extends StatefulWidget {
final Sheet sheet;
final ApiClient apiClient;
const SheetViewerPage({super.key, required this.sheet});
const SheetViewerPage(
{super.key, required this.sheet, required this.apiClient});
@override
State<SheetViewerPage> createState() => _SheetViewerPageState();
}
class _SheetViewerPageState extends State<SheetViewerPage> {
PdfController? controller;
@override
void initState() {
controller =
PdfController(document: PdfDocument.openFile(widget.sheet.path));
super.initState();
}
Future<PdfController> loadPdf() async {
var file = await widget.apiClient.getPdfFileCached(widget.sheet.uuid);
if (file == null) {
throw Exception("Failed fetching pdf file");
}
return PdfController(document: PdfDocument.openFile(file.path));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.sheet.name),
),
body: PdfView(
controller: controller!,
pageSnapping: false,
scrollDirection: Axis.vertical,
));
appBar: AppBar(
title: Text(widget.sheet.name),
),
body: FutureBuilder(
future: loadPdf(),
builder:
(BuildContext context, AsyncSnapshot<PdfController> snapshot) {
if (snapshot.hasData) {
return PdfView(
controller: snapshot.data!,
pageSnapping: false,
scrollDirection: Axis.vertical,
);
} 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());
}
}),
);
}
}