import 'dart:async'; import 'dart:convert'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf_router/shelf_router.dart'; import 'package:spotube/src/plugin_api/webview/webview.dart'; import 'package:async/async.dart'; class ServerWebviewRoutes { final Map _webviews = {}; Future postCreateWebview(Request request) async { final payload = jsonDecode(await request.readAsString()); final uri = Uri.parse(payload['url'] as String); final webview = Webview(uri: uri.toString()); _webviews[webview.uid] = webview; return Response.ok( jsonEncode({'uid': webview.uid}), encoding: utf8, headers: { 'Content-Type': 'application/json', }, ); } Future getOnUrlRequestStream(Request request) async { final uid = request.params["uid"]; final webview = _webviews[uid]; if (webview == null) { return Response.notFound('Webview with uid $uid not found'); } // Create a stream that merges URL events with keepalive pings final controller = StreamController>(); // Send keepalive comment every 15 seconds to prevent connection timeout final keepaliveTimer = Stream.periodic( const Duration(seconds: 15), (_) => utf8.encode(": keepalive\n\n"), ); final urlStream = webview.onUrlRequestStream.map((url) { return utf8.encode("event: url-request\n" "data: ${jsonEncode({'url': url})}\n\n"); }); // Merge both streams final subscription = StreamGroup.merge([keepaliveTimer, urlStream]).listen( (data) { if (!controller.isClosed) { controller.add(data); } }, onDone: () { controller.close(); }, ); // Clean up when client disconnects controller.onCancel = () { debugPrint('Webview $uid client disconnected'); subscription.cancel(); }; return Response.ok( controller.stream, headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', // Disable buffering for nginx }, encoding: utf8, ); } Future postOpenWebview(Request request) async { final body = jsonDecode(await request.readAsString()); final uid = body['uid'] as String; final webview = _webviews[uid]; if (webview == null) { return Response.notFound('Webview with uid $uid not found'); } await webview.open(); return Response.ok(null); } Future postCloseWebview(Request request) async { final body = jsonDecode(await request.readAsString()); final uid = body['uid'] as String; final webview = _webviews[uid]; if (webview == null) { return Response.notFound('Webview with uid $uid not found'); } await webview.close(); _webviews.remove(uid); return Response.ok(null); } Future postGetWebviewCookies(Request request) async { final body = jsonDecode(await request.readAsString()); final uid = body['uid'] as String; final url = body['url'] as String; final webview = _webviews[uid]; if (webview == null) { return Response.notFound('Webview with uid $uid not found'); } final cookies = await webview.getCookies(url); return Response.ok( jsonEncode(cookies), encoding: utf8, headers: { 'Content-Type': 'application/json', }, ); } Future dispose() async { for (final webview in _webviews.values) { await webview.close(); } _webviews.clear(); } } final serverWebviewRoutesProvider = Provider((ref) => ServerWebviewRoutes());