feat: add gallery feature with database integration and rendering capabilities
Build Android App / build (push) Failing after 2m19s
Build Android App / build (push) Failing after 2m19s
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import "dart:async";
|
||||
import "dart:io";
|
||||
import "dart:typed_data";
|
||||
import "dart:ui" as ui;
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/rendering.dart";
|
||||
import "package:path/path.dart" as path;
|
||||
import "package:path_provider/path_provider.dart";
|
||||
import "package:quotegen_client/services/quote_api_v2.dart";
|
||||
import "package:video_player/video_player.dart";
|
||||
|
||||
|
||||
class RenderProvider extends ChangeNotifier {
|
||||
|
||||
Uint8List? postPreviewImage;
|
||||
String? postPreviewContentType;
|
||||
|
||||
int renderProgress = 0;
|
||||
String renderStatus = "idle";
|
||||
String renderStage = "idle";
|
||||
|
||||
VideoPlayerController? videoController;
|
||||
bool videoLoadFailed = false;
|
||||
|
||||
StreamSubscription? _sseSubscription;
|
||||
bool _disposed = false;
|
||||
|
||||
|
||||
void clearRender() {
|
||||
if (postPreviewImage == null && renderStatus == "idle") return;
|
||||
|
||||
videoController?.dispose();
|
||||
videoController = null;
|
||||
|
||||
postPreviewImage = null;
|
||||
postPreviewContentType = null;
|
||||
renderStatus = "idle";
|
||||
renderProgress = 0;
|
||||
renderStage = "idle";
|
||||
videoLoadFailed = false;
|
||||
|
||||
if (!_disposed) notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> renderOutput(String? sessionId, bool isVideoInForm, {GlobalKey? previewKey}) async {
|
||||
if (!_disposed) {
|
||||
renderProgress = 0;
|
||||
renderStatus = "pending";
|
||||
renderStage = "idle";
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
if (isVideoInForm) {
|
||||
// video still goes through the server, so no session means theres genuinely nothing we can do
|
||||
if (sessionId == null) {
|
||||
print("Cant render video, session isnt ready (or never got created)");
|
||||
if (!_disposed) {
|
||||
renderStatus = "failed";
|
||||
notifyListeners();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
print("Starting video render for session: $sessionId");
|
||||
|
||||
if (!_disposed) {
|
||||
renderStatus = "rendering";
|
||||
renderStage = "rendering";
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// cancel any existing sub before starting new one
|
||||
await _sseSubscription?.cancel();
|
||||
|
||||
_sseSubscription = QuoteGeneratorApiV2.watchRenderProgress(sessionId).listen(
|
||||
(progress) {
|
||||
print("Render progress: ${progress.status} - ${progress.stage} - ${progress.progress}%");
|
||||
|
||||
if (_disposed) return;
|
||||
|
||||
renderProgress = progress.progress;
|
||||
renderStatus = progress.status;
|
||||
renderStage = progress.stage ?? "idle";
|
||||
notifyListeners();
|
||||
|
||||
if (progress.isComplete) {
|
||||
print("Render complete, fetching media...");
|
||||
fetchRenderedMedia(sessionId);
|
||||
} else if (progress.isFailed) {
|
||||
print("Render failed: ${progress.error}");
|
||||
if (!_disposed) {
|
||||
renderStatus = "failed";
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
print("Error watching render progress: $error");
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
await QuoteGeneratorApiV2.generateMedia(sessionId);
|
||||
} catch (e) {
|
||||
print("Error starting render: $e");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// image render - grab it straight off the live preview widget, dont need the server for this bit anymore
|
||||
try {
|
||||
if (previewKey == null) {
|
||||
throw Exception("no preview key given, cant capture anything");
|
||||
}
|
||||
|
||||
// renderStatus flipped to pending up top already, which swaps the ui back to the live
|
||||
// tweet card - just gotta wait for that frame to actually paint before we grab it
|
||||
await WidgetsBinding.instance.endOfFrame;
|
||||
|
||||
final bytes = await _capturePreview(previewKey);
|
||||
|
||||
if (_disposed) return;
|
||||
|
||||
videoController?.dispose();
|
||||
videoController = null;
|
||||
|
||||
postPreviewImage = bytes;
|
||||
postPreviewContentType = "image/png";
|
||||
videoLoadFailed = false;
|
||||
renderStatus = "completed";
|
||||
renderProgress = 100;
|
||||
renderStage = "completed";
|
||||
notifyListeners();
|
||||
|
||||
} catch (e) {
|
||||
print("Error rendering: $e");
|
||||
if (!_disposed) {
|
||||
renderStatus = "failed";
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rasterizes a repaint boundary to png, scaled to a fixed resolution regardless of its onscreen size
|
||||
Future<Uint8List> _capturePreview(GlobalKey key, {double targetSize = 1080}) async {
|
||||
final renderObj = key.currentContext?.findRenderObject();
|
||||
|
||||
if (renderObj is! RenderRepaintBoundary) {
|
||||
throw Exception("preview widget isnt mounted, nothing to capture");
|
||||
}
|
||||
|
||||
final logicalSize = renderObj.size;
|
||||
final pixelRatio = logicalSize.shortestSide == 0 ? 1.0 : targetSize / logicalSize.shortestSide;
|
||||
|
||||
final image = await renderObj.toImage(pixelRatio: pixelRatio);
|
||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
image.dispose();
|
||||
|
||||
if (byteData == null) {
|
||||
throw Exception("failed to encode captured png");
|
||||
}
|
||||
|
||||
return byteData.buffer.asUint8List();
|
||||
}
|
||||
|
||||
Future<void> fetchRenderedMedia(String sessionId) async {
|
||||
try {
|
||||
MediaResponse media = await QuoteGeneratorApiV2.generateMedia(sessionId);
|
||||
|
||||
if (_disposed) return;
|
||||
|
||||
videoController?.dispose();
|
||||
videoController = null;
|
||||
|
||||
postPreviewImage = media.bytes;
|
||||
postPreviewContentType = media.contentType;
|
||||
videoLoadFailed = false;
|
||||
renderStatus = "completed";
|
||||
renderProgress = 100;
|
||||
renderStage = "completed";
|
||||
notifyListeners();
|
||||
|
||||
if (media.isVideo) {
|
||||
await initializeVideoPlayer(media.bytes);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
print("Error fetching rendered media: $e");
|
||||
if (!_disposed) {
|
||||
renderStatus = "failed";
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initializeVideoPlayer(Uint8List videoBytes) async {
|
||||
try {
|
||||
videoLoadFailed = false;
|
||||
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final videoPath = path.join(tempDir.path, "preview_video_${DateTime.now().millisecondsSinceEpoch}.mp4");
|
||||
final file = File(videoPath);
|
||||
await file.writeAsBytes(videoBytes);
|
||||
|
||||
print("Video saved to: $videoPath (${videoBytes.length} bytes)");
|
||||
|
||||
videoController = VideoPlayerController.file(file);
|
||||
|
||||
videoController!.addListener(() {
|
||||
if (!_disposed) notifyListeners();
|
||||
});
|
||||
|
||||
await videoController!.initialize();
|
||||
|
||||
videoController!.setLooping(true);
|
||||
videoController!.play();
|
||||
|
||||
if (!_disposed) {
|
||||
videoLoadFailed = false;
|
||||
notifyListeners();
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error initializing video player: $e");
|
||||
if (!_disposed) {
|
||||
videoLoadFailed = true;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
_sseSubscription?.cancel();
|
||||
videoController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user