feat: add form api

This commit is contained in:
Kingkor Roy Tirtho 2025-12-07 17:00:27 +06:00
parent fe83f50286
commit 6da7fb7ac3
9 changed files with 102 additions and 19 deletions

View File

@ -202,16 +202,27 @@ class AuthEndpoint {
class CoreEndpoint { class CoreEndpoint {
async checkUpdate() { async checkUpdate() {
console.log(globalThis); console.log(globalThis);
const webview = await WebView.create("https://spotube.krtirtho.dev"); // const webview = await WebView.create("https://spotube.krtirtho.dev");
webview.onUrlChange((url) => { // webview.onUrlChange((url) => {
console.log("url_request: ", url); // console.log("url_request: ", url);
if (url.includes("/about")) { // if (url.includes("/about")) {
webview.close(); // 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 { class Plugin {

View File

@ -3,6 +3,7 @@ import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart'; import 'package:shelf_router/shelf_router.dart';
import 'package:spotube/provider/server/routes/connect.dart'; import 'package:spotube/provider/server/routes/connect.dart';
import 'package:spotube/provider/server/routes/playback.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'; import 'package:spotube/provider/server/routes/plugin_apis/webview.dart';
Handler pluginApiAuthMiddleware(Handler handler) { Handler pluginApiAuthMiddleware(Handler handler) {
@ -20,6 +21,7 @@ final serverRouterProvider = Provider((ref) {
final playbackRoutes = ref.watch(serverPlaybackRoutesProvider); final playbackRoutes = ref.watch(serverPlaybackRoutesProvider);
final connectRoutes = ref.watch(serverConnectRoutesProvider); final connectRoutes = ref.watch(serverConnectRoutesProvider);
final webviewRoutes = ref.watch(serverWebviewRoutesProvider); final webviewRoutes = ref.watch(serverWebviewRoutesProvider);
final formRoutes = ref.watch(serverFormRoutesProvider);
final router = Router(); final router = Router();
@ -52,6 +54,10 @@ final serverRouterProvider = Provider((ref) {
"/plugin-api/webview/cookies", "/plugin-api/webview/cookies",
pluginApiAuthMiddleware(webviewRoutes.postGetWebviewCookies), pluginApiAuthMiddleware(webviewRoutes.postGetWebviewCookies),
); );
router.post(
"/plugin-api/form/show",
pluginApiAuthMiddleware(formRoutes.showForm),
);
router.all("/ws", connectRoutes.websocket); router.all("/ws", connectRoutes.websocket);

View File

@ -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<Response> showForm(Request request) async {
final body = jsonDecode(await request.readAsString());
final res = await rootNavigatorKey.currentContext!.router
.push<List<Map<String, dynamic>>?>(
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());

View File

@ -82,7 +82,7 @@ packages:
source: hosted source: hosted
version: "1.6.5" version: "1.6.5"
async: async:
dependency: transitive dependency: "direct main"
description: description:
name: async name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"

View File

@ -162,6 +162,7 @@ dependencies:
flutter_rust_bridge: 2.11.1 flutter_rust_bridge: 2.11.1
json_annotation: ^4.9.0 json_annotation: ^4.9.0
random_user_agents: ^1.0.18 random_user_agents: ^1.0.18
async: ^2.13.0
dev_dependencies: dev_dependencies:
build_runner: ^2.4.13 build_runner: ^2.4.13

View File

@ -1,3 +1,4 @@
use std::fmt::format;
use crate::api::plugin::commands::PluginCommand; use crate::api::plugin::commands::PluginCommand;
use crate::api::plugin::executors::{ use crate::api::plugin::executors::{
execute_albums, execute_artists, execute_audio_source, execute_auth, execute_browse, execute_albums, execute_artists, execute_audio_source, execute_auth, execute_browse,
@ -11,7 +12,8 @@ use crate::api::plugin::senders::{
PluginTrackSender, PluginUserSender, PluginTrackSender, PluginUserSender,
}; };
use crate::frb_generated::StreamSink; use crate::frb_generated::StreamSink;
use crate::internal::apis::webview; use crate::internal::apis;
use crate::internal::apis::{form, webview};
use anyhow::anyhow; use anyhow::anyhow;
use flutter_rust_bridge::{frb, Rust2DartSendError}; use flutter_rust_bridge::{frb, Rust2DartSendError};
use llrt_modules::module_builder::ModuleBuilder; use llrt_modules::module_builder::ModuleBuilder;
@ -51,7 +53,9 @@ async fn create_context(
.with_global(navigator::init) .with_global(navigator::init)
.with_global(url::init) .with_global(url::init)
.with_global(timers::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(); let (module_resolver, module_loader, global_attachment) = module_builder.build();
runtime runtime
@ -63,8 +67,8 @@ async fn create_context(
.expect("Unable to create async context"); .expect("Unable to create async context");
async_with!(context => |ctx| { 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))?; 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(()) anyhow::Ok(())
}) })
.await .await

View File

@ -0,0 +1,25 @@
use rquickjs::{Ctx, Value};
pub fn init(ctx: &Ctx) -> rquickjs::Result<()> {
ctx.eval::<Value, _>(
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(())
}

View File

@ -1,2 +1,11 @@
use rquickjs::Ctx;
pub mod event_source; pub mod event_source;
pub mod form;
pub mod webview; 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(())
}

View File

@ -52,8 +52,8 @@ impl<'js> WebView<'js> {
#[qjs(static)] #[qjs(static)]
pub async fn create(ctx: Ctx<'js>, url: String) -> rquickjs::Result<Class<'js, WebView<'js>>> { pub async fn create(ctx: Ctx<'js>, url: String) -> rquickjs::Result<Class<'js, WebView<'js>>> {
let endpoint_url: String = ctx.globals().get("__webviewUrl").unwrap_or_default(); let endpoint_url: String = ctx.globals().get("__serverUrl").unwrap_or_default();
let secret: String = ctx.globals().get("__webviewSecret").unwrap_or_default(); let secret: String = ctx.globals().get("__serverSecret").unwrap_or_default();
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let endpoint = format!("{}/plugin-api/webview/create", endpoint_url.clone()); 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::<WebView>::define(&ctx.globals())?; Class::<WebView>::define(&ctx.globals())?;
Ok(()) Ok(())