From 6da7fb7ac361639455f2ad1fe16ff888c556d9f9 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 7 Dec 2025 17:00:27 +0600 Subject: [PATCH] feat: add form api --- lib/main.dart | 27 ++++++++++++----- lib/provider/server/router.dart | 6 ++++ .../server/routes/plugin_apis/form.dart | 30 +++++++++++++++++++ pubspec.lock | 2 +- pubspec.yaml | 1 + rust/src/api/plugin/plugin.rs | 10 +++++-- rust/src/internal/apis/form.rs | 25 ++++++++++++++++ rust/src/internal/apis/mod.rs | 9 ++++++ rust/src/internal/apis/webview.rs | 11 +++---- 9 files changed, 102 insertions(+), 19 deletions(-) create mode 100644 lib/provider/server/routes/plugin_apis/form.dart create mode 100644 rust/src/internal/apis/form.rs diff --git a/lib/main.dart b/lib/main.dart index b0cb3a8f..e871ae26 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -202,16 +202,27 @@ class AuthEndpoint { class CoreEndpoint { async checkUpdate() { console.log(globalThis); - const webview = await WebView.create("https://spotube.krtirtho.dev"); - webview.onUrlChange((url) => { - console.log("url_request: ", url); - if (url.includes("/about")) { - webview.close(); + // const webview = await WebView.create("https://spotube.krtirtho.dev"); + // webview.onUrlChange((url) => { + // console.log("url_request: ", url); + // if (url.includes("/about")) { + // webview.close(); + // } + // }); + // await webview.open(); + const res = await SpotubeForm.show("Hello", [ + { + objectType: "input", + id: "email", + variant: "text", + placeholder: "Enter your email", + defaultValue: null, + required: true, + regex: null, } - }); - await webview.open(); + ]) - await new Promise((resolve) => setTimeout(resolve, 5000)); + console.log("Form Result: ", res); } } class Plugin { diff --git a/lib/provider/server/router.dart b/lib/provider/server/router.dart index 0851be80..8ad786d2 100644 --- a/lib/provider/server/router.dart +++ b/lib/provider/server/router.dart @@ -3,6 +3,7 @@ import 'package:shelf/shelf.dart'; import 'package:shelf_router/shelf_router.dart'; import 'package:spotube/provider/server/routes/connect.dart'; import 'package:spotube/provider/server/routes/playback.dart'; +import 'package:spotube/provider/server/routes/plugin_apis/form.dart'; import 'package:spotube/provider/server/routes/plugin_apis/webview.dart'; Handler pluginApiAuthMiddleware(Handler handler) { @@ -20,6 +21,7 @@ final serverRouterProvider = Provider((ref) { final playbackRoutes = ref.watch(serverPlaybackRoutesProvider); final connectRoutes = ref.watch(serverConnectRoutesProvider); final webviewRoutes = ref.watch(serverWebviewRoutesProvider); + final formRoutes = ref.watch(serverFormRoutesProvider); final router = Router(); @@ -52,6 +54,10 @@ final serverRouterProvider = Provider((ref) { "/plugin-api/webview/cookies", pluginApiAuthMiddleware(webviewRoutes.postGetWebviewCookies), ); + router.post( + "/plugin-api/form/show", + pluginApiAuthMiddleware(formRoutes.showForm), + ); router.all("/ws", connectRoutes.websocket); diff --git a/lib/provider/server/routes/plugin_apis/form.dart b/lib/provider/server/routes/plugin_apis/form.dart new file mode 100644 index 00000000..e124f966 --- /dev/null +++ b/lib/provider/server/routes/plugin_apis/form.dart @@ -0,0 +1,30 @@ +import 'dart:convert'; + +import 'package:auto_route/auto_route.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:shelf/shelf.dart'; +import 'package:spotube/collections/routes.dart'; +import 'package:spotube/collections/routes.gr.dart'; +import 'package:spotube/models/metadata/metadata.dart'; + +class ServerFormRoutes { + Future showForm(Request request) async { + final body = jsonDecode(await request.readAsString()); + final res = await rootNavigatorKey.currentContext!.router + .push>?>( + SettingsMetadataProviderFormRoute( + title: body["title"], + fields: (body["fields"] as List) + .map((e) => MetadataFormFieldObject.fromJson(e)) + .toList(), + ), + ); + + return Response.ok( + jsonEncode(res), + headers: {'Content-Type': 'application/json'}, + ); + } +} + +final serverFormRoutesProvider = Provider((ref) => ServerFormRoutes()); diff --git a/pubspec.lock b/pubspec.lock index eb8cce1a..1479273a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -82,7 +82,7 @@ packages: source: hosted version: "1.6.5" async: - dependency: transitive + dependency: "direct main" description: name: async sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" diff --git a/pubspec.yaml b/pubspec.yaml index 31becd97..0a5690f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -162,6 +162,7 @@ dependencies: flutter_rust_bridge: 2.11.1 json_annotation: ^4.9.0 random_user_agents: ^1.0.18 + async: ^2.13.0 dev_dependencies: build_runner: ^2.4.13 diff --git a/rust/src/api/plugin/plugin.rs b/rust/src/api/plugin/plugin.rs index d6f14223..d6116c8b 100644 --- a/rust/src/api/plugin/plugin.rs +++ b/rust/src/api/plugin/plugin.rs @@ -1,3 +1,4 @@ +use std::fmt::format; use crate::api::plugin::commands::PluginCommand; use crate::api::plugin::executors::{ execute_albums, execute_artists, execute_audio_source, execute_auth, execute_browse, @@ -11,7 +12,8 @@ use crate::api::plugin::senders::{ PluginTrackSender, PluginUserSender, }; use crate::frb_generated::StreamSink; -use crate::internal::apis::webview; +use crate::internal::apis; +use crate::internal::apis::{form, webview}; use anyhow::anyhow; use flutter_rust_bridge::{frb, Rust2DartSendError}; use llrt_modules::module_builder::ModuleBuilder; @@ -51,7 +53,9 @@ async fn create_context( .with_global(navigator::init) .with_global(url::init) .with_global(timers::init) - .with_global(util::init); + .with_global(util::init) + .with_global(form::init) + .with_global(webview::init); let (module_resolver, module_loader, global_attachment) = module_builder.build(); runtime @@ -63,8 +67,8 @@ async fn create_context( .expect("Unable to create async context"); async_with!(context => |ctx| { + apis::init(&ctx, server_endpoint_url, server_secret)?; global_attachment.attach(&ctx).catch(&ctx).map_err(|e| anyhow!("Failed to attach global modules: {}", e))?; - webview::init(&ctx, server_endpoint_url, server_secret).catch(&ctx).map_err(|e| anyhow!("Failed to initialize WebView API: {}", e))?; anyhow::Ok(()) }) .await diff --git a/rust/src/internal/apis/form.rs b/rust/src/internal/apis/form.rs new file mode 100644 index 00000000..b8d3d309 --- /dev/null +++ b/rust/src/internal/apis/form.rs @@ -0,0 +1,25 @@ +use rquickjs::{Ctx, Value}; + +pub fn init(ctx: &Ctx) -> rquickjs::Result<()> { + ctx.eval::( + r#" + globalThis.SpotubeForm = class SpotubeForm { + static async show(title, fields) { + return await fetch( + `${__serverUrl}/plugin-api/form/show`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Plugin-Secret': __serverSecret, + }, + body: JSON.stringify({ title, fields }), + } + ).then(res=>res.json()); + } + } + "#, + )?; + + Ok(()) +} diff --git a/rust/src/internal/apis/mod.rs b/rust/src/internal/apis/mod.rs index 9f6f93c1..6c02f88b 100644 --- a/rust/src/internal/apis/mod.rs +++ b/rust/src/internal/apis/mod.rs @@ -1,2 +1,11 @@ +use rquickjs::Ctx; + pub mod event_source; +pub mod form; pub mod webview; + +pub fn init(ctx: &Ctx, endpoint_url: String, secret: String) -> rquickjs::Result<()> { + ctx.globals().set("__serverUrl", endpoint_url)?; + ctx.globals().set("__serverSecret", secret)?; + Ok(()) +} diff --git a/rust/src/internal/apis/webview.rs b/rust/src/internal/apis/webview.rs index 3b46c2fe..ab3da917 100644 --- a/rust/src/internal/apis/webview.rs +++ b/rust/src/internal/apis/webview.rs @@ -52,8 +52,8 @@ impl<'js> WebView<'js> { #[qjs(static)] pub async fn create(ctx: Ctx<'js>, url: String) -> rquickjs::Result>> { - let endpoint_url: String = ctx.globals().get("__webviewUrl").unwrap_or_default(); - let secret: String = ctx.globals().get("__webviewSecret").unwrap_or_default(); + let endpoint_url: String = ctx.globals().get("__serverUrl").unwrap_or_default(); + let secret: String = ctx.globals().get("__serverSecret").unwrap_or_default(); let client = reqwest::Client::new(); let endpoint = format!("{}/plugin-api/webview/create", endpoint_url.clone()); @@ -203,12 +203,9 @@ impl<'js> WebView<'js> { } } -pub fn init(ctx: &Ctx, endpoint_url: String, secret: String) -> rquickjs::Result<()> { - // Store config in globals for access in static methods - ctx.globals().set("__webviewUrl", endpoint_url)?; - ctx.globals().set("__webviewSecret", secret)?; - // Register the WebView class + +pub fn init(ctx: &Ctx) -> rquickjs::Result<()> { Class::::define(&ctx.globals())?; Ok(())