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 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 _buildHeaders({bool isBinary = false}) { return { 'Authorization': 'Bearer $token', if (!isBinary) 'Content-Type': 'application/json', }; } /// Performs a GET request to the given endpoint. Future 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 post( String endpoint, Map 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 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> fetchSheets() async { final response = await get('/api/sheets/list'); final data = jsonDecode(response.body); return (data['sheets'] as List) .map((sheet) => Sheet.fromJson(sheet as Map)) .toList(); } /// Downloads PDF data for a sheet. /// /// Returns the raw PDF bytes. For caching, use [getPdfFileCached] instead. Future 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 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'; }