Work on implementing sheetable client
This commit is contained in:
206
lib/api.dart
Normal file
206
lib/api.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user