Complete refactor to clean up project
This commit is contained in:
202
lib/core/services/api_client.dart
Normal file
202
lib/core/services/api_client.dart
Normal file
@@ -0,0 +1,202 @@
|
||||
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';
|
||||
}
|
||||
Reference in New Issue
Block a user