203 lines
5.8 KiB
Dart
203 lines
5.8 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:logging/logging.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
import '../models/sheet.dart';
|
|
|
|
/// HTTP client for communicating with the Sheetless API server.
|
|
///
|
|
/// Handles authentication, sheet listing, and PDF downloads.
|
|
/// Provides caching for PDF files on native platforms.
|
|
class ApiClient {
|
|
final _log = Logger('ApiClient');
|
|
final String baseUrl;
|
|
String? token;
|
|
|
|
ApiClient({required this.baseUrl, this.token});
|
|
|
|
/// Whether the client has an authentication token set.
|
|
bool get isAuthenticated => token != null;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Authentication
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Authenticates with the server and stores the JWT token.
|
|
///
|
|
/// Throws an [Exception] if login fails.
|
|
Future<void> login(String username, String password) async {
|
|
_log.info('Logging in...');
|
|
final url = Uri.parse('$baseUrl/auth/login');
|
|
|
|
final response = await http.post(
|
|
url,
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode({'username': username, 'password': password}),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
final responseData = jsonDecode(response.body);
|
|
token = responseData['token'];
|
|
_log.info('Login successful');
|
|
} else {
|
|
throw ApiException(
|
|
'Login failed',
|
|
statusCode: response.statusCode,
|
|
body: response.body,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Clears the authentication token.
|
|
void logout() {
|
|
token = null;
|
|
_log.info('Logged out successfully');
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// HTTP Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
Map<String, String> _buildHeaders({bool isBinary = false}) {
|
|
return {
|
|
'Authorization': 'Bearer $token',
|
|
if (!isBinary) 'Content-Type': 'application/json',
|
|
};
|
|
}
|
|
|
|
/// Performs a GET request to the given endpoint.
|
|
Future<http.Response> get(
|
|
String endpoint, {
|
|
bool isBinary = false,
|
|
}) async {
|
|
final url = Uri.parse('$baseUrl$endpoint');
|
|
final response =
|
|
await http.get(url, headers: _buildHeaders(isBinary: isBinary));
|
|
|
|
if (response.statusCode != 200) {
|
|
_log.warning(
|
|
"GET '$endpoint' failed: ${response.statusCode}\n${response.body}",
|
|
);
|
|
throw ApiException(
|
|
'GET request failed',
|
|
statusCode: response.statusCode,
|
|
body: response.body,
|
|
);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
/// Performs a POST request with JSON body.
|
|
Future<http.Response> post(
|
|
String endpoint,
|
|
Map<String, dynamic> body,
|
|
) async {
|
|
final url = Uri.parse('$baseUrl$endpoint');
|
|
|
|
final response = await http.post(
|
|
url,
|
|
headers: _buildHeaders(),
|
|
body: jsonEncode(body),
|
|
);
|
|
|
|
if (response.statusCode != 200 && response.statusCode != 201) {
|
|
_log.warning(
|
|
"POST '$endpoint' failed: ${response.statusCode}\n${response.body}",
|
|
);
|
|
throw ApiException(
|
|
'POST request failed',
|
|
statusCode: response.statusCode,
|
|
body: response.body,
|
|
);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
/// Performs a POST request with form-encoded body.
|
|
Future<http.Response> postFormData(String endpoint, String body) async {
|
|
final url = Uri.parse('$baseUrl$endpoint');
|
|
|
|
final response = await http.post(
|
|
url,
|
|
headers: {
|
|
'Authorization': 'Bearer $token',
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: body,
|
|
);
|
|
|
|
if (response.statusCode != 200 && response.statusCode != 201) {
|
|
_log.warning(
|
|
"POST Form '$endpoint' failed: ${response.statusCode}\n${response.body}",
|
|
);
|
|
throw ApiException(
|
|
'POST Form request failed',
|
|
statusCode: response.statusCode,
|
|
body: response.body,
|
|
);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Sheet Operations
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Fetches the list of all sheets from the server.
|
|
Future<List<Sheet>> fetchSheets() async {
|
|
final response = await get('/api/sheets/list');
|
|
final data = jsonDecode(response.body);
|
|
|
|
return (data['sheets'] as List<dynamic>)
|
|
.map((sheet) => Sheet.fromJson(sheet as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
|
|
/// Downloads PDF data for a sheet.
|
|
///
|
|
/// Returns the raw PDF bytes. For caching, use [getPdfFileCached] instead.
|
|
Future<Uint8List> fetchPdfData(String sheetUuid) async {
|
|
final response = await get('/api/sheets/get/$sheetUuid', isBinary: true);
|
|
_log.info('PDF downloaded for sheet $sheetUuid');
|
|
return response.bodyBytes;
|
|
}
|
|
|
|
/// Gets a cached PDF file for a sheet, downloading if necessary.
|
|
///
|
|
/// On web platforms, use [fetchPdfData] instead as file caching
|
|
/// is not supported.
|
|
Future<File> getPdfFileCached(String sheetUuid) async {
|
|
final cacheDir = await getTemporaryDirectory();
|
|
final cachedFile = File('${cacheDir.path}/$sheetUuid.pdf');
|
|
|
|
if (await cachedFile.exists()) {
|
|
_log.info('PDF found in cache: ${cachedFile.path}');
|
|
return cachedFile;
|
|
}
|
|
|
|
final pdfData = await fetchPdfData(sheetUuid);
|
|
await cachedFile.writeAsBytes(pdfData);
|
|
_log.info('PDF cached at: ${cachedFile.path}');
|
|
return cachedFile;
|
|
}
|
|
}
|
|
|
|
/// Exception thrown when an API request fails.
|
|
class ApiException implements Exception {
|
|
final String message;
|
|
final int statusCode;
|
|
final String body;
|
|
|
|
ApiException(this.message, {required this.statusCode, required this.body});
|
|
|
|
@override
|
|
String toString() => 'ApiException: $message (status: $statusCode)\n$body';
|
|
}
|