Added download

This commit is contained in:
verboomp
2026-02-03 09:51:03 +01:00
parent f9ca668b39
commit 5f1d2d8610
25 changed files with 874 additions and 145 deletions

View File

@@ -11,18 +11,10 @@ import 'package:fotodocumentation/utils/url_utils.dart';
abstract class BaseController {
UrlUtils get uriUtils => DiContainer.get();
JwtTokenStorage get _jwtTokenStorage => DiContainer.get();
HttpClientUtils get httpClientUtils => DiContainer.get();
Future<Header> getAuthHeader() async {
final accessToken = await _jwtTokenStorage.getAccessToken();
if (accessToken != null && accessToken.isNotEmpty) {
// Use JWT Bearer token
return Header('Authorization', 'Bearer $accessToken');
} else {
return const Header("Accept-Language", "en-US");
}
Header getAuthHeader() {
return HeaderUtils().getAuthHeader();
}
Exception getServerError(Response response) {
@@ -32,7 +24,7 @@ abstract class BaseController {
Future<List<T>> runGetListWithAuth<T>(String uriStr, List<T> Function(dynamic) convert) async {
http.Client client = httpClientUtils.client;
try {
Header cred = await getAuthHeader();
Header cred = getAuthHeader();
Uri uri = Uri.parse(uriStr);
var response = await client.get(uri, headers: {cred.name: cred.value});
if (response.statusCode == 200) {
@@ -51,7 +43,7 @@ abstract class BaseController {
Future<T?> runGetWithAuth<T>(String uriStr, T Function(dynamic) convert) async {
http.Client client = httpClientUtils.client;
try {
Header cred = await getAuthHeader();
Header cred = getAuthHeader();
Uri uri = Uri.parse(uriStr);
var response = await client.get(uri, headers: {cred.name: cred.value});
if (response.statusCode == 200) {
@@ -67,9 +59,26 @@ abstract class BaseController {
}
}
Future<List<int>> runGetBytesWithAuth(String uriStr) async {
http.Client client = httpClientUtils.client;
try {
Header cred = getAuthHeader();
Uri uri = Uri.parse(uriStr);
var response = await client.get(uri, headers: {cred.name: cred.value});
if (response.statusCode == 200) {
return response.bodyBytes;
} else {
throw ServerError(response.statusCode);
}
} catch (e) {
logger.e("exception $e");
rethrow;
}
}
Future<bool> runDeleteWithAuth(String uriStr) async {
http.Client client = httpClientUtils.client;
Header cred = await getAuthHeader();
Header cred = getAuthHeader();
Uri uri = Uri.parse(uriStr);
var response = await client.delete(uri, headers: {cred.name: cred.value});
return response.statusCode == 200;
@@ -77,13 +86,28 @@ abstract class BaseController {
Future<bool> runPutWithAuth(String uriStr) async {
http.Client client = httpClientUtils.client;
Header cred = await getAuthHeader();
Header cred = getAuthHeader();
Uri uri = Uri.parse(uriStr);
var response = await client.put(uri, headers: {cred.name: cred.value});
return response.statusCode == 200;
}
}
class HeaderUtils{
JwtTokenStorage get _jwtTokenStorage => DiContainer.get();
Header getAuthHeader() {
final accessToken = _jwtTokenStorage.getAccessToken();
if (accessToken != null && accessToken.isNotEmpty) {
// Use JWT Bearer token
return Header('Authorization', 'Bearer $accessToken');
} else {
return const Header("Accept-Language", "en-US");
}
}
}
class Header {
final String name;
final String value;

View File

@@ -5,6 +5,8 @@ abstract interface class CustomerController {
Future<List<CustomerListDto>> getAll(String query, String startsWith);
Future<CustomerDto?> get({required int id});
Future<List<int>> export({required int customerId, int? pictureId});
}
class CustomerControllerImpl extends BaseController implements CustomerController {
@@ -28,4 +30,13 @@ class CustomerControllerImpl extends BaseController implements CustomerControlle
String uriStr = '${uriUtils.getBaseUrl()}$path/$id';
return runGetWithAuth(uriStr, (json) => CustomerDto.fromJson(json));
}
@override
Future<List<int>> export({required int customerId, int? pictureId}) {
String uriStr = '${uriUtils.getBaseUrl()}$path/export/$customerId';
if (pictureId != null) {
uriStr += '?picture=$pictureId';
}
return runGetBytesWithAuth(uriStr);
}
}

View File

@@ -13,7 +13,7 @@ typedef AuthenticateReply = ({JwtTokenPairDto? jwtTokenPairDto});
abstract interface class LoginController {
Future<AuthenticateReply> authenticate(String username, String password);
Future<bool> refreshAccessToken();
Future<bool> isUsingJwtAuth();
bool isUsingJwtAuth();
}
class LoginControllerImpl extends BaseController implements LoginController {
@@ -36,7 +36,7 @@ class LoginControllerImpl extends BaseController implements LoginController {
final tokenPair = JwtTokenPairDto.fromJson(data);
// Store tokens securely
await _jwtTokenStorage.saveTokens(tokenPair.accessToken, tokenPair.refreshToken);
_jwtTokenStorage.saveTokens(tokenPair.accessToken, tokenPair.refreshToken);
// Load user data using the new token
return (jwtTokenPairDto: tokenPair);
@@ -53,7 +53,7 @@ class LoginControllerImpl extends BaseController implements LoginController {
@override
Future<bool> refreshAccessToken() async {
try {
final refreshToken = await _jwtTokenStorage.getRefreshToken();
final refreshToken = _jwtTokenStorage.getRefreshToken();
if (refreshToken == null) {
logger.i('No refresh token available');
return false;
@@ -74,7 +74,7 @@ class LoginControllerImpl extends BaseController implements LoginController {
final newAccessToken = data['accessToken'] as String;
// Update only the access token (keep same refresh token)
await _jwtTokenStorage.updateAccessToken(newAccessToken);
_jwtTokenStorage.updateAccessToken(newAccessToken);
logger.d('Access token refreshed successfully');
return true;
@@ -89,8 +89,8 @@ class LoginControllerImpl extends BaseController implements LoginController {
}
@override
Future<bool> isUsingJwtAuth() async {
return await _jwtTokenStorage.hasTokens();
bool isUsingJwtAuth() {
return _jwtTokenStorage.hasTokens();
}
Header _getLoginHeader(String username, String password) {

View File

@@ -22,7 +22,7 @@ void main() async {
await initializeDateFormatting('de_DE', null);
LoginController loginController = DiContainer.get();
await loginController.isUsingJwtAuth();
loginController.isUsingJwtAuth();
runApp(FotoDocumentationApp(theme: theme));
}

View File

@@ -14,6 +14,7 @@ import 'package:fotodocumentation/pages/ui_utils/general_style.dart';
import 'package:fotodocumentation/utils/di_container.dart';
import 'package:fotodocumentation/utils/global_router.dart';
import 'package:go_router/go_router.dart';
import 'package:fotodocumentation/utils/file_download.dart';
import 'package:intl/intl.dart';
class CustomerWidget extends StatefulWidget {
@@ -202,6 +203,7 @@ class _CustomerWidgetState extends State<CustomerWidget> {
final dateStr = _dateFormat.format(pictureDto.pictureDate);
final evaluationColor = _generalStyle.evaluationColor(value: pictureDto.evaluation);
Header cred = HeaderUtils().getAuthHeader();
return InkWell(
key: Key("table_row_${customerDto.id}"),
onTap: () => _actionSelect(context, customerDto, pictureDto),
@@ -219,6 +221,7 @@ class _CustomerWidgetState extends State<CustomerWidget> {
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 70, maxHeight: 70),
child: Image.network(
headers: {cred.name: cred.value},
pictureDto.thumbnailSizeUrl,
fit: BoxFit.contain,
),
@@ -334,6 +337,10 @@ class _CustomerWidgetState extends State<CustomerWidget> {
}
Future<void> _actionDownload(BuildContext context, CustomerDto customerDto, PictureDto? pictureDto) async {
// FIXME: implement a download from the export
final bytes = await _customerController.export(customerId: customerDto.id, pictureId: pictureDto?.id);
final fileName = pictureDto != null
? '${customerDto.customerNumber}_${pictureDto.id}.pdf'
: '${customerDto.customerNumber}.pdf';
await downloadFile(bytes, fileName);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:fotodocumentation/controller/base_controller.dart';
import 'package:fotodocumentation/dto/picture_dto.dart';
import 'package:fotodocumentation/pages/ui_utils/general_style.dart';
import 'package:fotodocumentation/utils/di_container.dart';
@@ -15,7 +16,8 @@ class PictureFullscreenDialog extends StatelessWidget {
final screenSize = MediaQuery.of(context).size;
final dialogWidth = screenSize.width * 0.9;
final dialogHeight = screenSize.height * 0.9 - 50;
Header cred = HeaderUtils().getAuthHeader();
return Dialog(
backgroundColor: Colors.black,
clipBehavior: Clip.hardEdge,
@@ -50,6 +52,7 @@ class PictureFullscreenDialog extends StatelessWidget {
minScale: 0.5,
maxScale: 5.0,
child: Image.network(
headers: {cred.name: cred.value},
dto.imageUrl,
),
),

View File

@@ -144,6 +144,8 @@ class _PictureWidgetState extends State<PictureWidget> {
}
Widget _imageWidget(PictureDto dto) {
Header cred = HeaderUtils().getAuthHeader();
return GestureDetector(
key: const Key("image"),
behavior: HitTestBehavior.opaque,
@@ -153,6 +155,7 @@ class _PictureWidgetState extends State<PictureWidget> {
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100, minHeight: 100),
child: Image.network(
headers: {cred.name: cred.value},
dto.normalSizeUrl,
fit: BoxFit.contain,
),
@@ -176,16 +179,17 @@ class _PictureWidgetState extends State<PictureWidget> {
color: _generalStyle.secondaryTextLabelColor,
);
String dateText = _dateFormat.format(dto.pictureDate);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_dateFormat.format(dto.pictureDate),
"$dateText UHR",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 44,
fontSize: 32,
fontFamily: _generalStyle.fontFamily,
color: _generalStyle.primaryTextLabelColor,
color: _generalStyle.secondaryWidgetBackgroundColor,
),
),
const SizedBox(height: 20),

View File

@@ -0,0 +1 @@
export 'file_download_stub.dart' if (dart.library.js) 'file_download_web.dart' if (dart.library.io) 'file_download_app.dart';

View File

@@ -0,0 +1,3 @@
Future<void> downloadFile(List<int> bytes, String fileName) {
throw UnsupportedError('File download not supported on this platform');
}

View File

@@ -0,0 +1,3 @@
Future<void> downloadFile(List<int> bytes, String fileName) {
throw UnsupportedError('Cannot download file on this platform');
}

View File

@@ -0,0 +1,13 @@
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
Future<void> downloadFile(List<int> bytes, String fileName) async {
await FilePicker.platform.saveFile(
dialogTitle: fileName,
fileName: fileName,
type: FileType.custom,
allowedExtensions: ['pdf'],
bytes: Uint8List.fromList(bytes),
);
}

View File

@@ -3,30 +3,30 @@ abstract class JwtTokenStorage {
///
/// @param accessToken The short-lived access token
/// @param refreshToken The long-lived refresh token
Future<void> saveTokens(String accessToken, String refreshToken);
void saveTokens(String accessToken, String refreshToken);
/// Get the stored access token
///
/// @return Access token or null if not found
Future<String?> getAccessToken();
String? getAccessToken();
/// Get the stored refresh token
///
/// @return Refresh token or null if not found
Future<String?> getRefreshToken();
String? getRefreshToken();
/// Clear all stored tokens (on logout)
Future<void> clearTokens();
void clearTokens();
/// Check if tokens are stored
///
/// @return true if access token exists
Future<bool> hasTokens();
bool hasTokens();
/// Update only the access token (used after refresh)
///
/// @param accessToken New access token
Future<void> updateAccessToken(String accessToken);
void updateAccessToken(String accessToken);
}
class JwtTokenStorageImpl extends JwtTokenStorage {
@@ -36,34 +36,34 @@ class JwtTokenStorageImpl extends JwtTokenStorage {
String? _keyRefreshToken;
@override
Future<void> saveTokens(String accessToken, String refreshToken) async {
void saveTokens(String accessToken, String refreshToken) async {
_keyAccessToken = accessToken;
_keyRefreshToken = refreshToken;
}
@override
Future<String?> getAccessToken() async {
String? getAccessToken() {
return _keyAccessToken;
}
@override
Future<String?> getRefreshToken() async {
String? getRefreshToken() {
return _keyRefreshToken;
}
@override
Future<void> clearTokens() async {
void clearTokens() {
_keyAccessToken = null;
_keyRefreshToken = null;
}
@override
Future<bool> hasTokens() async {
bool hasTokens() {
return _keyAccessToken != null && _keyAccessToken!.isNotEmpty;
}
@override
Future<void> updateAccessToken(String accessToken) async {
void updateAccessToken(String accessToken) {
_keyAccessToken == accessToken;
}
}