diff --git a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart index 95f0582..a30e30b 100644 --- a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart @@ -652,10 +652,20 @@ class PacksApiV2 { objectId: voicePath, ); + if (presignedUrl == null) { + print('⚠️ Warning: Failed to generate presigned URL for voice ${voice.id} with objectId $voicePath'); + // Fallback to API endpoint if presigned URL generation fails + return _voiceModelToDto( + voice, + path: voicePath, + url: _voiceAbsoluteUrl(request, voice.id), + ).toJson(); + } + return _voiceModelToDto( voice, path: voicePath, - url: presignedUrl ?? _voiceAbsoluteUrl(request, voice.id), + url: presignedUrl, ).toJson(); } diff --git a/mnemo_cards_web_v2/lib/domain/services/http_repository_v2.dart b/mnemo_cards_web_v2/lib/domain/services/http_repository_v2.dart index 51baa9d..85f238f 100644 --- a/mnemo_cards_web_v2/lib/domain/services/http_repository_v2.dart +++ b/mnemo_cards_web_v2/lib/domain/services/http_repository_v2.dart @@ -1010,6 +1010,54 @@ class HttpRepositoryV2 { } } + /// Download bytes from a URL (e.g., presigned URL from MinIO). + /// This method doesn't require authentication as presigned URLs are public. + /// Uses a separate Dio instance without interceptors to avoid adding auth headers. + Future downloadBytesFromUrl(String url) async { + try { + // Create a separate Dio instance without interceptors for presigned URLs + final dio = Dio( + BaseOptions( + connectTimeout: ApiConfigV2.connectionTimeout, + receiveTimeout: ApiConfigV2.requestTimeout, + ), + ); + + final response = await dio.get>( + url, + options: Options( + responseType: ResponseType.bytes, + followRedirects: true, + validateStatus: (status) => status != null && status < 500, + ), + ); + + if (response.statusCode != 200) { + throw ServerException( + message: 'Failed to download from URL: ${response.statusCode}', + statusCode: response.statusCode ?? 500, + ); + } + + final data = response.data; + if (data == null) { + throw const ServerException( + message: 'Empty response from URL', + statusCode: 500, + ); + } + return Uint8List.fromList(data); + } on DioException catch (e) { + if (e.error is ApiException) { + rethrow; + } + throw NetworkException( + message: e.message ?? 'Network error', + originalError: e, + ); + } + } + /// Get pack purchase details (includes ad reward offers) Future getPackBuy(String packId) async { try { diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/card_voice_controls.dart b/mnemo_cards_web_v2/lib/presentation/widgets/card_voice_controls.dart index 4251b92..a27ee5d 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/card_voice_controls.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/card_voice_controls.dart @@ -1,4 +1,5 @@ import 'dart:developer'; +import 'dart:typed_data'; import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/material.dart'; @@ -87,12 +88,37 @@ class _CardVoiceControlsState extends State { throw Exception('Scope not available'); } - // Flutter Web can't attach Authorization headers to the underlying - // `