feat: weird impl

This commit is contained in:
Kingkor Roy Tirtho 2025-12-03 18:25:48 +06:00
parent 7b0c49f565
commit fca0551032
20 changed files with 2661 additions and 383 deletions

View File

@ -45,6 +45,7 @@ import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/wm_tools/wm_tools.dart'; import 'package:spotube/services/wm_tools/wm_tools.dart';
import 'package:spotube/src/plugin_api/webview/webview_binding.dart';
import 'package:spotube/src/rust/api/plugin/models/core.dart'; import 'package:spotube/src/rust/api/plugin/models/core.dart';
import 'package:spotube/src/rust/api/plugin/plugin.dart'; import 'package:spotube/src/rust/api/plugin/plugin.dart';
import 'package:spotube/src/rust/frb_generated.dart'; import 'package:spotube/src/rust/frb_generated.dart';
@ -58,28 +59,6 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:yt_dlp_dart/yt_dlp_dart.dart'; import 'package:yt_dlp_dart/yt_dlp_dart.dart';
import 'package:flutter_new_pipe_extractor/flutter_new_pipe_extractor.dart'; import 'package:flutter_new_pipe_extractor/flutter_new_pipe_extractor.dart';
const pluginJS = """
function timeout(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
class CoreEndpoint {
async checkUpdate() {
console.log('Core checkUpdate');
await timeout(5000);
console.log('Core checkUpdate done. No updates!');
}
get support() {
return 'Metadata';
}
}
class TestingPlugin {
constructor() {
this.core = new CoreEndpoint();
}
}
""";
Future<void> main(List<String> rawArgs) async { Future<void> main(List<String> rawArgs) async {
if (rawArgs.contains("web_view_title_bar")) { if (rawArgs.contains("web_view_title_bar")) {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -120,25 +99,44 @@ Future<void> main(List<String> rawArgs) async {
await KVStoreService.initialize(); await KVStoreService.initialize();
await RustLib.init(); await RustLib.init();
WebViewBinding.register();
final plugin = SpotubePlugin(); final plugin = SpotubePlugin();
const config = PluginConfiguration( const pluginConfiguration = PluginConfiguration(
entryPoint: "TestingPlugin", name: "Spotube Plugin",
abilities: [PluginAbility.metadata], description: "Spotube Plugin",
apis: [], version: "1.0.0",
author: "KRTirtho", author: "Spotube",
description: "Testing Plugin", entryPoint: "Plugin",
name: "Testing Plugin",
pluginApiVersion: "2.0.0", pluginApiVersion: "2.0.0",
repository: null, apis: [PluginApi.localstorage, PluginApi.webview],
version: "0.1.0", abilities: [PluginAbility.metadata],
); );
final sender = plugin.createContext( final pluginContext = plugin.createContext(
pluginScript: pluginJS, pluginScript: """
pluginConfig: config, class AuthEndpoint {
}
class CoreEndpoint {
async checkUpdate() {
const webview = await Webview.create("https://spotube.krtirtho.dev");
webview.events.on("url_change", (url) => {
console.log("url_change: ", url);
})
await webview.open();
}
}
class Plugin {
constructor() {
this.auth = new AuthEndpoint();
this.core = new CoreEndpoint();
}
}
""",
pluginConfig: pluginConfiguration,
); );
await plugin.core.checkUpdate(mpscTx: sender, pluginConfig: config); await plugin.core
.checkUpdate(mpscTx: pluginContext, pluginConfig: pluginConfiguration);
if (kIsDesktop) { if (kIsDesktop) {
await windowManager.setPreventClose(true); await windowManager.setPreventClose(true);

View File

@ -0,0 +1,119 @@
import 'dart:async';
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:desktop_webview_window/desktop_webview_window.dart'
as webview_window;
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:random_user_agents/random_user_agents.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' hide join;
import 'package:spotube/collections/routes.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/src/plugin_api/webview/webview_page.dart';
import 'package:uuid/uuid.dart';
class Webview {
final String uri;
final String uid;
Webview({
required this.uri,
}) : _onUrlRequestStreamController = StreamController<String>.broadcast(),
uid = const Uuid().v4();
StreamController<String>? _onUrlRequestStreamController;
Stream<String> get onUrlRequestStream =>
_onUrlRequestStreamController!.stream;
webview_window.Webview? _webview;
BuildContext? _pageContext;
Future<void> open() async {
if (Platform.isLinux) {
final applicationSupportDir = await getApplicationSupportDirectory();
final userDataFolder = Directory(
join(applicationSupportDir.path, "webview_window_Webview2"),
);
if (!await userDataFolder.exists()) {
await userDataFolder.create();
}
_webview = await WebviewWindow.create(
configuration: CreateConfiguration(
title: "Spotube Login",
windowHeight: 720,
windowWidth: 1280,
userDataFolderWindows: userDataFolder.path,
),
)
..setApplicationUserAgent(RandomUserAgents.random());
_webview!.setOnUrlRequestCallback((url) {
_onUrlRequestStreamController?.add(url);
return true;
});
_webview!.launch(uri);
return;
}
final route = WebviewPage(
uri: uri,
onLoad: (url) {
_onUrlRequestStreamController?.add(url.toString());
},
);
await rootNavigatorKey.currentContext?.router.pushWidget(
Builder(builder: (context) {
_pageContext = context;
return Scaffold(
headers: const [
TitleBar(
automaticallyImplyLeading: true,
)
],
child: route,
);
}),
);
}
Future<void> close() async {
_onUrlRequestStreamController?.close();
_onUrlRequestStreamController = null;
if (Platform.isLinux) {
_webview?.close();
_webview = null;
return;
}
await _pageContext?.maybePop();
}
Future<List<Cookie>> getCookies(String url) async {
if (Platform.isLinux) {
final cookies = await _webview?.getAllCookies() ?? [];
return cookies.map((cookie) {
return Cookie(
name: cookie.name,
value: cookie.value,
domain: cookie.domain,
expiresDate: cookie.expires?.millisecondsSinceEpoch,
isHttpOnly: cookie.httpOnly,
isSecure: cookie.secure,
isSessionOnly: cookie.sessionOnly,
path: cookie.path,
);
}).toList();
}
return await CookieManager.instance(
// Created in [WebviewPage]. Custom WebViewEnvironment for Windows otherwise it installs
// in installation directory so permission exception occurs.
webViewEnvironment: await webViewEnvironment,
).getCookies(url: WebUri(url));
}
}

View File

@ -0,0 +1,40 @@
import 'dart:async';
import 'dart:convert';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/src/plugin_api/webview/webview.dart';
import 'package:spotube/src/rust/api/host_api/webview.dart';
class WebViewBinding {
static void register() async {
final subscriptions = <String, StreamSubscription>{};
await initializeWebviewCallbacks(
createWebview: (uri, sender) async {
final webview = Webview(uri: uri);
subscriptions[webview.uid] =
webview.onUrlRequestStream.listen((event) async {
try {
await sendWebviewEvents(tx: sender, event: event);
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
});
return webview;
},
openWebview: (webview) async {
await (webview as Webview).open();
},
closeWebview: (webview) async {
subscriptions.remove((webview as Webview).uid);
await webview.close();
},
getCookies: (webview, url) async {
final cookies = await (webview as Webview).getCookies(url);
return jsonEncode(cookies);
},
);
}
}

View File

@ -0,0 +1,62 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:fk_user_agent/fk_user_agent.dart';
Future<String?> getUserAgent() async {
if (Platform.isIOS || Platform.isAndroid) {
await FkUserAgent.init();
return FkUserAgent.userAgent;
}
return null;
}
final webViewEnvironment = Platform.isWindows
? getApplicationSupportDirectory().then((directory) async {
return await WebViewEnvironment.create(
settings: WebViewEnvironmentSettings(
userDataFolder: join(directory.path, 'inappwebview_data'),
),
);
})
: Future.value(null);
class WebviewPage extends StatelessWidget {
final String uri;
final void Function(String url)? onLoad;
const WebviewPage({super.key, required this.uri, this.onLoad});
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: Future.wait([webViewEnvironment, getUserAgent()]),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
return InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(uri)),
webViewEnvironment: snapshot.data?[0] as WebViewEnvironment?,
initialSettings: InAppWebViewSettings(
userAgent: snapshot.data?[1] as String?,
),
onLoadStop: (controller, url) {
try {
if (onLoad != null && url != null) {
onLoad!(url.toString());
}
} catch (e, stack) {
debugPrint("[Webview][onLoad] Error: $e");
debugPrintStack(stackTrace: stack);
rethrow;
}
},
);
},
);
}
}

View File

@ -0,0 +1,31 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `DART_CLOSE_WEBVIEW`, `DART_CREATE_WEBVIEW`, `DART_GET_COOKIES`, `DART_OPEN_WEBVIEW`, `HostWebview`, `Webview`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `deref`, `deref`, `deref`, `deref`, `initialize`, `initialize`, `initialize`, `initialize`, `trace`
// These functions are ignored (category: IgnoreBecauseExplicitAttribute): `close`, `close`, `create`, `create`, `get_cookies`, `get_cookies`, `open`, `open`, `poll_url_change_event`
Future<void> initializeWebviewCallbacks(
{required FutureOr<Object> Function(String, BroadcastSenderString)
createWebview,
required FutureOr<void> Function(Object) openWebview,
required FutureOr<void> Function(Object) closeWebview,
required FutureOr<String> Function(Object, String) getCookies}) =>
RustLib.instance.api.crateApiHostApiWebviewInitializeWebviewCallbacks(
createWebview: createWebview,
openWebview: openWebview,
closeWebview: closeWebview,
getCookies: getCookies);
Future<void> sendWebviewEvents(
{required BroadcastSenderString tx, required String event}) =>
RustLib.instance.api
.crateApiHostApiWebviewSendWebviewEvents(tx: tx, event: event);
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<BroadcastSender < String >>>
abstract class BroadcastSenderString implements RustOpaqueInterface {}

View File

@ -12,6 +12,7 @@ import 'senders.dart';
// These functions are ignored because they are not marked as `pub`: `create_context`, `js_executor_thread` // These functions are ignored because they are not marked as `pub`: `create_context`, `js_executor_thread`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `fmt` // These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `fmt`
// These functions are ignored (category: IgnoreBecauseExplicitAttribute): `open_webview`
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<OpaqueSender>> // Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<OpaqueSender>>
abstract class OpaqueSender implements RustOpaqueInterface { abstract class OpaqueSender implements RustOpaqueInterface {
@ -66,7 +67,7 @@ abstract class SpotubePlugin implements RustOpaqueInterface {
Future<void> close({required OpaqueSender tx}); Future<void> close({required OpaqueSender tx});
Future<OpaqueSender> createContext( OpaqueSender createContext(
{required String pluginScript, {required String pluginScript,
required PluginConfiguration pluginConfig}); required PluginConfiguration pluginConfig});

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field // ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
import 'api/host_api/webview.dart';
import 'api/plugin/commands.dart'; import 'api/plugin/commands.dart';
import 'api/plugin/models/album.dart'; import 'api/plugin/models/album.dart';
import 'api/plugin/models/artist.dart'; import 'api/plugin/models/artist.dart';
@ -33,6 +34,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
required super.portManager, required super.portManager,
}); });
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_BroadcastSenderStringPtr => wire
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderStringPtr;
CrossPlatformFinalizerArg CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_OpaqueSenderPtr => wire get rust_arc_decrement_strong_count_OpaqueSenderPtr => wire
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSenderPtr; ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSenderPtr;
@ -52,6 +57,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
AnyhowException dco_decode_AnyhowException(dynamic raw); AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
BroadcastSenderString
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderString(
dynamic raw);
@protected @protected
OpaqueSender OpaqueSender
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender( dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
@ -92,6 +102,29 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin( dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
dynamic raw); dynamic raw);
@protected
FutureOr<void> Function(Object)
dco_decode_DartFn_Inputs_DartOpaque_Output_unit_AnyhowException(
dynamic raw);
@protected
FutureOr<String> Function(Object, String)
dco_decode_DartFn_Inputs_DartOpaque_String_Output_String_AnyhowException(
dynamic raw);
@protected
FutureOr<Object> Function(String, BroadcastSenderString)
dco_decode_DartFn_Inputs_String_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderString_Output_DartOpaque_AnyhowException(
dynamic raw);
@protected
Object dco_decode_DartOpaque(dynamic raw);
@protected
BroadcastSenderString
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderString(
dynamic raw);
@protected @protected
OpaqueSender OpaqueSender
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender( dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
@ -242,6 +275,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
PlatformInt64 dco_decode_i_64(dynamic raw); PlatformInt64 dco_decode_i_64(dynamic raw);
@protected
PlatformInt64 dco_decode_isize(dynamic raw);
@protected @protected
List<String> dco_decode_list_String(dynamic raw); List<String> dco_decode_list_String(dynamic raw);
@ -485,6 +521,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
BroadcastSenderString
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderString(
SseDeserializer deserializer);
@protected @protected
OpaqueSender OpaqueSender
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender( sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
@ -525,6 +566,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin( sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
SseDeserializer deserializer); SseDeserializer deserializer);
@protected
Object sse_decode_DartOpaque(SseDeserializer deserializer);
@protected
BroadcastSenderString
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderString(
SseDeserializer deserializer);
@protected @protected
OpaqueSender OpaqueSender
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender( sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
@ -691,6 +740,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer); PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
@protected
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
@protected @protected
List<String> sse_decode_list_String(SseDeserializer deserializer); List<String> sse_decode_list_String(SseDeserializer deserializer);
@ -968,6 +1020,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
void sse_encode_AnyhowException( void sse_encode_AnyhowException(
AnyhowException self, SseSerializer serializer); AnyhowException self, SseSerializer serializer);
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderString(
BroadcastSenderString self, SseSerializer serializer);
@protected @protected
void void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender( sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
@ -1008,6 +1065,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin( sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
SpotubePlugin self, SseSerializer serializer); SpotubePlugin self, SseSerializer serializer);
@protected
void sse_encode_DartFn_Inputs_DartOpaque_Output_unit_AnyhowException(
FutureOr<void> Function(Object) self, SseSerializer serializer);
@protected
void sse_encode_DartFn_Inputs_DartOpaque_String_Output_String_AnyhowException(
FutureOr<String> Function(Object, String) self, SseSerializer serializer);
@protected
void
sse_encode_DartFn_Inputs_String_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderString_Output_DartOpaque_AnyhowException(
FutureOr<Object> Function(String, BroadcastSenderString) self,
SseSerializer serializer);
@protected
void sse_encode_DartOpaque(Object self, SseSerializer serializer);
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderString(
BroadcastSenderString self, SseSerializer serializer);
@protected @protected
void void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender( sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
@ -1170,6 +1249,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer); void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
@protected @protected
void sse_encode_list_String(List<String> self, SseSerializer serializer); void sse_encode_list_String(List<String> self, SseSerializer serializer);
@ -1456,6 +1538,38 @@ class RustLibWire implements BaseWire {
RustLibWire(ffi.DynamicLibrary dynamicLibrary) RustLibWire(ffi.DynamicLibrary dynamicLibrary)
: _lookup = dynamicLibrary.lookup; : _lookup = dynamicLibrary.lookup;
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderString(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderString(
ptr,
);
}
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderStringPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_spotube_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderString');
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderString =
_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderStringPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderString(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderString(
ptr,
);
}
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderStringPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_spotube_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderString');
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderString =
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBroadcastSenderStringPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender( rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
ffi.Pointer<ffi.Void> ptr, ffi.Pointer<ffi.Void> ptr,

View File

@ -742,7 +742,7 @@ packages:
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
fk_user_agent: fk_user_agent:
dependency: transitive dependency: "direct main"
description: description:
path: "." path: "."
ref: master ref: master
@ -1935,13 +1935,13 @@ packages:
source: hosted source: hosted
version: "3.2.2" version: "3.2.2"
random_user_agents: random_user_agents:
dependency: transitive dependency: "direct main"
description: description:
name: random_user_agents name: random_user_agents
sha256: "95647149687167e82a7b39e1b4616fdebb574981b71b6f0cfca21b69f36293a8" sha256: "80dc025723a73f04797351aa6ef2fddb14836f86a752711a2f8a04e37c4ccdff"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.17" version: "1.0.18"
recase: recase:
dependency: transitive dependency: transitive
description: description:

View File

@ -42,6 +42,10 @@ dependencies:
envied: ^1.0.0 envied: ^1.0.0
file_picker: 10.3.3 file_picker: 10.3.3
file_selector: ^1.0.3 file_selector: ^1.0.3
fk_user_agent:
git:
url: https://github.com/TiffApps/fk_user_agent.git
ref: master
fluentui_system_icons: ^1.1.234 fluentui_system_icons: ^1.1.234
flutter: flutter:
sdk: flutter sdk: flutter
@ -157,6 +161,7 @@ dependencies:
path: rust_builder path: rust_builder
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
dev_dependencies: dev_dependencies:
build_runner: ^2.4.13 build_runner: ^2.4.13

99
rust/Cargo.lock generated
View File

@ -191,6 +191,26 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
[[package]]
name = "bindgen"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.10.0" version = "2.10.0"
@ -293,6 +313,15 @@ dependencies = [
"shlex", "shlex",
] ]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.4" version = "1.0.4"
@ -334,6 +363,17 @@ dependencies = [
"inout", "inout",
] ]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]] [[package]]
name = "cmake" name = "cmake"
version = "0.1.54" version = "0.1.54"
@ -898,6 +938,12 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.4.12" version = "0.4.12"
@ -1266,6 +1312,15 @@ dependencies = [
"hybrid-array", "hybrid-array",
] ]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.15" version = "1.0.15"
@ -1304,6 +1359,16 @@ version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libloading"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "libm" name = "libm"
version = "0.2.15" version = "0.2.15"
@ -1909,6 +1974,12 @@ version = "2.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.7.1" version = "0.7.1"
@ -1939,6 +2010,16 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -2175,6 +2256,16 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "prettyplease"
version = "0.2.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2"
dependencies = [
"proc-macro2",
"syn",
]
[[package]] [[package]]
name = "primefield" name = "primefield"
version = "0.14.0-rc.1" version = "0.14.0-rc.1"
@ -2396,6 +2487,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57b1b6528590d4d65dc86b5159eae2d0219709546644c66408b2441696d1d725" checksum = "57b1b6528590d4d65dc86b5159eae2d0219709546644c66408b2441696d1d725"
dependencies = [ dependencies = [
"bindgen",
"cc", "cc",
] ]
@ -2428,6 +2520,7 @@ dependencies = [
"flutter_rust_bridge", "flutter_rust_bridge",
"heck", "heck",
"llrt_modules", "llrt_modules",
"once_cell",
"rquickjs", "rquickjs",
"serde", "serde",
"serde_json", "serde_json",
@ -2440,6 +2533,12 @@ version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"

View File

@ -15,10 +15,11 @@ flutter_rust_bridge = "=2.11.1"
anyhow = "1" anyhow = "1"
serde_json = "1" serde_json = "1"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
rquickjs = { version = "0", features = ["chrono", "futures"] } rquickjs = { version = "0", features = ["chrono", "futures", "macro", "classes", "bindgen"] }
tokio = { version = "1.48.0", features = ["full"] } tokio = { version = "1.48.0", features = ["full"] }
heck = "0.5.0" heck = "0.5.0"
llrt_modules = { git = "https://github.com/awslabs/llrt.git", rev = "7d749dd18cf26a2e51119094c3b945975ae57bd4", features = ["abort", "buffer", "console", "crypto", "events", "exceptions", "fetch", "navigator", "url", "timers"] } llrt_modules = { git = "https://github.com/awslabs/llrt.git", rev = "7d749dd18cf26a2e51119094c3b945975ae57bd4", features = ["abort", "buffer", "console", "crypto", "events", "exceptions", "fetch", "navigator", "url", "timers"] }
once_cell = "1.21.3"
[patch."https://github.com/DelSkayn/rquickjs"] [patch."https://github.com/DelSkayn/rquickjs"]
rquickjs = "0.10.0" rquickjs = "0.10.0"

View File

@ -0,0 +1 @@
pub mod webview;

View File

@ -0,0 +1,191 @@
use anyhow::anyhow;
use flutter_rust_bridge::for_generated::lazy_static;
use flutter_rust_bridge::{frb, DartFnFuture, DartOpaque};
use llrt_modules::events::EventEmitter;
use rquickjs::function::This;
use rquickjs::{Class, Function, Object};
use std::sync::Mutex;
use tokio::sync::broadcast;
pub type BroadcastSender<T> = broadcast::Sender<T>;
pub type BroadcastReceiver<T> = broadcast::Receiver<T>;
lazy_static! {
static ref DART_CREATE_WEBVIEW: Mutex<
Option<
Box<
dyn Fn(String, BroadcastSender<String>) -> DartFnFuture<DartOpaque>
+ Send
+ 'static,
>,
>,
> = Mutex::new(None);
static ref DART_OPEN_WEBVIEW: Mutex<Option<Box<dyn Fn(DartOpaque) -> DartFnFuture<()> + Send + 'static>>> =
Mutex::new(None);
static ref DART_CLOSE_WEBVIEW: Mutex<Option<Box<dyn Fn(DartOpaque) -> DartFnFuture<()> + Send + 'static>>> =
Mutex::new(None);
static ref DART_GET_COOKIES: Mutex<Option<Box<dyn Fn(DartOpaque, String) -> DartFnFuture<String> + Send + 'static>>> =
Mutex::new(None);
}
pub async fn initialize_webview_callbacks(
create_webview: impl Fn(String, BroadcastSender<String>) -> DartFnFuture<DartOpaque>
+ Send
+ 'static,
open_webview: impl Fn(DartOpaque) -> DartFnFuture<()> + Send + 'static,
close_webview: impl Fn(DartOpaque) -> DartFnFuture<()> + Send + 'static,
get_cookies: impl Fn(DartOpaque, String) -> DartFnFuture<String> + Send + 'static,
) -> anyhow::Result<()> {
*DART_CREATE_WEBVIEW
.lock()
.map_err(|_| anyhow!("Mutex poisoned"))? = Some(Box::new(create_webview));
*DART_OPEN_WEBVIEW
.lock()
.map_err(|_| anyhow!("Mutex poisoned"))? = Some(Box::new(open_webview));
*DART_CLOSE_WEBVIEW
.lock()
.map_err(|_| anyhow!("Mutex poisoned"))? = Some(Box::new(close_webview));
*DART_GET_COOKIES
.lock()
.map_err(|_| anyhow!("Mutex poisoned"))? = Some(Box::new(get_cookies));
Ok(())
}
pub async fn send_webview_events(tx: BroadcastSender<String>, event: String) -> anyhow::Result<()> {
tx.send(event)
.map_err(|_| anyhow!("Failed to send event"))?;
Ok(())
}
#[frb(ignore)]
pub struct HostWebview {
webview: DartOpaque,
events: BroadcastReceiver<String>,
}
#[frb(ignore)]
impl HostWebview {
pub async fn create(uri: String) -> anyhow::Result<Self> {
let (tx, rx) = broadcast::channel(100);
let s = DART_CREATE_WEBVIEW
.lock()
.map_err(|_| anyhow!("Mutex poisoned"))?;
if let Some(create_webview_fn) = s.as_ref() {
let s = create_webview_fn(uri, tx.clone()).await;
Ok(Self {
webview: s,
events: rx,
})
} else {
Err(anyhow!("create_webview not implemented"))
}
}
pub async fn open(&self) -> anyhow::Result<()> {
let s = DART_OPEN_WEBVIEW
.lock()
.map_err(|_| anyhow!("Mutex poisoned"))?;
if let Some(open_webview) = s.as_ref() {
open_webview(self.webview.clone()).await;
Ok(())
} else {
Err(anyhow!("open_webview not implemented"))
}
}
pub async fn close(&self) -> anyhow::Result<()> {
let s = DART_CLOSE_WEBVIEW
.lock()
.map_err(|_| anyhow!("Mutex poisoned"))?;
if let Some(close_webview) = s.as_ref() {
close_webview(self.webview.clone()).await;
Ok(())
} else {
Err(anyhow!("close_webview not implemented"))
}
}
pub async fn get_cookies(&self, url: String) -> anyhow::Result<String> {
let s = DART_GET_COOKIES
.lock()
.map_err(|_| anyhow!("Mutex poisoned"))?;
if let Some(get_cookies) = s.as_ref() {
let s = get_cookies(self.webview.clone(), url).await;
Ok(s)
} else {
Err(anyhow!("get_cookies not implemented"))
}
}
}
#[frb(ignore)]
#[rquickjs::class]
#[derive(rquickjs::JsLifetime, rquickjs::class::Trace)]
pub struct Webview {
#[qjs(skip_trace)]
webview: HostWebview,
}
#[frb(ignore)]
#[rquickjs::methods(rename_all = "camelCase")]
impl Webview {
#[qjs(static)]
pub async fn create(uri: String) -> rquickjs::Result<Self> {
let webview = HostWebview::create(uri)
.await
.map_err(|_| rquickjs::Error::Exception)?;
Ok(Self { webview })
}
pub async fn open(&mut self, this: This<Class<'_, Self>>) -> rquickjs::Result<()> {
let mut events = this.get::<_, Object>("events")?;
if events.is_null() || events.is_undefined() {
this.set("events", EventEmitter::new())?;
events = this.get::<_, Object>("events")?;
}
let emit = events.clone().get::<_, Function>("emit")?;
let mut rx = self.webview.events.resubscribe();
this.ctx().spawn(async move {
while let Ok(event) = rx.recv().await {
if let Err(e) = emit.call::<_, ()>(("url_change", event)) {
eprintln!("Failed to emit event: {:?}", e);
}
}
});
self.webview
.open()
.await
.map_err(|_| rquickjs::Error::Exception)?;
Ok(())
}
pub async fn close(&self) -> rquickjs::Result<()> {
self.webview
.close()
.await
.map_err(|_| rquickjs::Error::Exception)?;
Ok(())
}
pub async fn get_cookies(&self, url: String) -> rquickjs::Result<String> {
self.webview
.get_cookies(url)
.await
.map_err(|_| rquickjs::Error::Exception)
}
pub async fn poll_url_change_event(&mut self) -> rquickjs::Result<String> {
let event = self
.webview
.events
.recv()
.await
.map_err(|_| rquickjs::Error::Exception)?;
Ok(event)
}
}

View File

@ -1,4 +1,5 @@
pub mod plugin; pub mod plugin;
pub mod host_api;
#[flutter_rust_bridge::frb(init)] #[flutter_rust_bridge::frb(init)]
pub fn init_app() { pub fn init_app() {

View File

@ -17,19 +17,26 @@ use llrt_modules::module_builder::ModuleBuilder;
use llrt_modules::{ use llrt_modules::{
abort, buffer, console, crypto, events, exceptions, fetch, navigator, timers, url, util, abort, buffer, console, crypto, events, exceptions, fetch, navigator, timers, url, util,
}; };
use rquickjs::prelude::Func; use rquickjs::prelude::{Async, Func};
use rquickjs::{async_with, AsyncContext, AsyncRuntime, Error, Object}; use rquickjs::{async_with, AsyncContext, AsyncRuntime, Class, Error, Object};
use std::thread; use std::thread;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
use tokio::task; use tokio::task;
use tokio::task::LocalSet; use tokio::task::LocalSet;
use crate::api::host_api::webview::{HostWebview, Webview};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct OpaqueSender { pub struct OpaqueSender {
pub sender: Sender<PluginCommand>, pub sender: Sender<PluginCommand>,
} }
#[frb(ignore)]
pub async fn open_webview(uri: String){
let webview = HostWebview::create(uri).await.unwrap();
webview.open().await.unwrap();
}
#[frb(ignore)] #[frb(ignore)]
async fn create_context() -> anyhow::Result<(AsyncContext, AsyncRuntime)> { async fn create_context() -> anyhow::Result<(AsyncContext, AsyncRuntime)> {
let runtime = AsyncRuntime::new().expect("Unable to create async runtime"); let runtime = AsyncRuntime::new().expect("Unable to create async runtime");
@ -60,6 +67,13 @@ async fn create_context() -> anyhow::Result<(AsyncContext, AsyncRuntime)> {
async_with!(context => |ctx| { async_with!(context => |ctx| {
global_attachment.attach(&ctx)?; global_attachment.attach(&ctx)?;
let global = ctx.globals();
Class::<Webview>::define(&global)?;
let globals = ctx.globals();
globals.set("openWebview", Func::new(Async(open_webview)))?;
Ok::<(), Error>(()) Ok::<(), Error>(())
}) })
.await .await
@ -70,7 +84,7 @@ async fn create_context() -> anyhow::Result<(AsyncContext, AsyncRuntime)> {
#[frb(ignore)] #[frb(ignore)]
async fn js_executor_thread( async fn js_executor_thread(
rx: &mut Receiver<PluginCommand>, rx: &mut Receiver<PluginCommand>,
ctx: &AsyncContext, context: &AsyncContext,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
while let Some(command) = rx.recv().await { while let Some(command) = rx.recv().await {
println!("JS Executor thread received command: {:?}", command); println!("JS Executor thread received command: {:?}", command);
@ -79,19 +93,19 @@ async fn js_executor_thread(
return anyhow::Ok(()); return anyhow::Ok(());
} }
let ctx = ctx.clone(); let context = context.clone();
task::spawn_local(async move { task::spawn_local(async move {
let result = match command { let result = match command {
PluginCommand::Artist(commands) => execute_artists(commands, &ctx).await, PluginCommand::Artist(commands) => execute_artists(commands, &context).await,
PluginCommand::Album(commands) => execute_albums(commands, &ctx).await, PluginCommand::Album(commands) => execute_albums(commands, &context).await,
PluginCommand::AudioSource(commands) => execute_audio_source(commands, &ctx).await, PluginCommand::AudioSource(commands) => execute_audio_source(commands, &context).await,
PluginCommand::Auth(commands) => execute_auth(commands, &ctx).await, PluginCommand::Auth(commands) => execute_auth(commands, &context).await,
PluginCommand::Browse(commands) => execute_browse(commands, &ctx).await, PluginCommand::Browse(commands) => execute_browse(commands, &context).await,
PluginCommand::Core(commands) => execute_core(commands, &ctx).await, PluginCommand::Core(commands) => execute_core(commands, &context).await,
PluginCommand::Playlist(commands) => execute_playlist(commands, &ctx).await, PluginCommand::Playlist(commands) => execute_playlist(commands, &context).await,
PluginCommand::Search(commands) => execute_search(commands, &ctx).await, PluginCommand::Search(commands) => execute_search(commands, &context).await,
PluginCommand::Track(commands) => execute_track(commands, &ctx).await, PluginCommand::Track(commands) => execute_track(commands, &context).await,
PluginCommand::User(commands) => execute_user(commands, &ctx).await, PluginCommand::User(commands) => execute_user(commands, &context).await,
PluginCommand::Shutdown => unreachable!(), PluginCommand::Shutdown => unreachable!(),
}; };
@ -148,7 +162,7 @@ impl SpotubePlugin {
Ok(()) Ok(())
} }
// #[frb(sync)] #[frb(sync)]
pub fn create_context( pub fn create_context(
&self, &self,
plugin_script: String, plugin_script: String,

View File

@ -1,4 +1,3 @@
use std::backtrace::Backtrace;
use crate::api::plugin::commands::{ use crate::api::plugin::commands::{
AlbumCommands, ArtistCommands, AudioSourceCommands, AuthCommands, BrowseCommands, CoreCommands, AlbumCommands, ArtistCommands, AudioSourceCommands, AuthCommands, BrowseCommands, CoreCommands,
PlaylistCommands, PluginCommand, SearchCommands, TrackCommands, UserCommands, PlaylistCommands, PluginCommand, SearchCommands, TrackCommands, UserCommands,
@ -19,6 +18,7 @@ use crate::api::plugin::models::user::SpotubeUserObject;
use crate::api::plugin::plugin::OpaqueSender; use crate::api::plugin::plugin::OpaqueSender;
use anyhow::anyhow; use anyhow::anyhow;
use flutter_rust_bridge::frb; use flutter_rust_bridge::frb;
use std::backtrace::Backtrace;
use tokio::sync::oneshot; use tokio::sync::oneshot;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -32,7 +32,7 @@ impl PluginArtistSender {
pub async fn get_artist( pub async fn get_artist(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
id: String, id: String,
) -> anyhow::Result<SpotubeFullArtistObject> { ) -> anyhow::Result<SpotubeFullArtistObject> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
@ -49,7 +49,7 @@ impl PluginArtistSender {
pub async fn top_tracks( pub async fn top_tracks(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
id: String, id: String,
offset: Option<u32>, offset: Option<u32>,
limit: Option<u32>, limit: Option<u32>,
@ -70,7 +70,7 @@ impl PluginArtistSender {
pub async fn albums( pub async fn albums(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
id: String, id: String,
offset: Option<u32>, offset: Option<u32>,
limit: Option<u32>, limit: Option<u32>,
@ -91,7 +91,7 @@ impl PluginArtistSender {
pub async fn related( pub async fn related(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
id: String, id: String,
offset: Option<u32>, offset: Option<u32>,
limit: Option<u32>, limit: Option<u32>,
@ -110,7 +110,7 @@ impl PluginArtistSender {
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o) rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
} }
pub async fn save(&self, mpsc_tx: OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> { pub async fn save(&self, mpsc_tx: &OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
mpsc_tx mpsc_tx
.sender .sender
@ -123,7 +123,7 @@ impl PluginArtistSender {
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o) rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
} }
pub async fn unsave(&self, mpsc_tx: OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> { pub async fn unsave(&self, mpsc_tx: &OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
mpsc_tx mpsc_tx
.sender .sender
@ -148,7 +148,7 @@ impl PluginAlbumSender {
pub async fn get_album( pub async fn get_album(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
id: String, id: String,
) -> anyhow::Result<SpotubeFullAlbumObject> { ) -> anyhow::Result<SpotubeFullAlbumObject> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
@ -165,7 +165,7 @@ impl PluginAlbumSender {
pub async fn tracks( pub async fn tracks(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
id: String, id: String,
offset: Option<u32>, offset: Option<u32>,
limit: Option<u32>, limit: Option<u32>,
@ -186,7 +186,7 @@ impl PluginAlbumSender {
pub async fn releases( pub async fn releases(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
offset: Option<u32>, offset: Option<u32>,
limit: Option<u32>, limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> { ) -> anyhow::Result<SpotubePaginationResponseObject> {
@ -203,7 +203,7 @@ impl PluginAlbumSender {
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o) rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
} }
pub async fn save(&self, mpsc_tx: OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> { pub async fn save(&self, mpsc_tx: &OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
mpsc_tx mpsc_tx
.sender .sender
@ -216,7 +216,7 @@ impl PluginAlbumSender {
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o) rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
} }
pub async fn unsave(&self, mpsc_tx: OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> { pub async fn unsave(&self, mpsc_tx: &OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
mpsc_tx mpsc_tx
.sender .sender
@ -241,7 +241,7 @@ impl PluginAudioSourceSender {
pub async fn matches( pub async fn matches(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
track: SpotubeTrackObject, track: SpotubeTrackObject,
) -> anyhow::Result<Vec<SpotubeAudioSourceMatchObject>> { ) -> anyhow::Result<Vec<SpotubeAudioSourceMatchObject>> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
@ -258,7 +258,7 @@ impl PluginAudioSourceSender {
pub async fn streams( pub async fn streams(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
matched: SpotubeAudioSourceMatchObject, matched: SpotubeAudioSourceMatchObject,
) -> anyhow::Result<Vec<SpotubeAudioSourceStreamObject>> { ) -> anyhow::Result<Vec<SpotubeAudioSourceStreamObject>> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
@ -283,7 +283,7 @@ impl PluginAuthSender {
Self {} Self {}
} }
pub async fn authenticate(&self, mpsc_tx: OpaqueSender) -> anyhow::Result<()> { pub async fn authenticate(&self, mpsc_tx: &OpaqueSender) -> anyhow::Result<()> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
mpsc_tx mpsc_tx
.sender .sender
@ -295,7 +295,7 @@ impl PluginAuthSender {
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o) rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
} }
pub async fn logout(&self, mpsc_tx: OpaqueSender) -> anyhow::Result<()> { pub async fn logout(&self, mpsc_tx: &OpaqueSender) -> anyhow::Result<()> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
mpsc_tx mpsc_tx
.sender .sender
@ -307,7 +307,7 @@ impl PluginAuthSender {
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o) rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
} }
pub async fn is_authenticated(&self, mpsc_tx: OpaqueSender) -> anyhow::Result<bool> { pub async fn is_authenticated(&self, mpsc_tx: &OpaqueSender) -> anyhow::Result<bool> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
mpsc_tx mpsc_tx
.sender .sender
@ -331,7 +331,7 @@ impl PluginBrowseSender {
pub async fn sections( pub async fn sections(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
offset: Option<u32>, offset: Option<u32>,
limit: Option<u32>, limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> { ) -> anyhow::Result<SpotubePaginationResponseObject> {
@ -350,7 +350,7 @@ impl PluginBrowseSender {
pub async fn section_items( pub async fn section_items(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
id: String, id: String,
offset: Option<u32>, offset: Option<u32>,
limit: Option<u32>, limit: Option<u32>,
@ -381,10 +381,9 @@ impl PluginCoreSender {
pub async fn check_update( pub async fn check_update(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
plugin_config: PluginConfiguration, plugin_config: PluginConfiguration,
) -> anyhow::Result<Option<PluginUpdateAvailable>> { ) -> anyhow::Result<Option<PluginUpdateAvailable>> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
mpsc_tx mpsc_tx
.sender .sender
@ -394,14 +393,16 @@ impl PluginCoreSender {
})) }))
.await?; .await?;
rx.await.map_err(|e| { rx.await
eprintln!("RecvError: {}", e); .map_err(|e| {
eprintln!("Stack trace:\n{:?}", Backtrace::capture()); eprintln!("RecvError: {}", e);
anyhow!("{e}") eprintln!("Stack trace:\n{:?}", Backtrace::capture());
}).and_then(|o| o) anyhow!("{e}")
})
.and_then(|o| o)
} }
pub async fn support(&self, mpsc_tx: OpaqueSender) -> anyhow::Result<String> { pub async fn support(&self, mpsc_tx: &OpaqueSender) -> anyhow::Result<String> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
mpsc_tx mpsc_tx
.sender .sender
@ -415,7 +416,7 @@ impl PluginCoreSender {
pub async fn scrobble( pub async fn scrobble(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
details: ScrobbleDetails, details: ScrobbleDetails,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
@ -442,7 +443,7 @@ impl PluginPlaylistSender {
pub async fn get_playlist( pub async fn get_playlist(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
id: String, id: String,
) -> anyhow::Result<SpotubeFullPlaylistObject> { ) -> anyhow::Result<SpotubeFullPlaylistObject> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
@ -459,7 +460,7 @@ impl PluginPlaylistSender {
pub async fn tracks( pub async fn tracks(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
id: String, id: String,
offset: Option<u32>, offset: Option<u32>,
limit: Option<u32>, limit: Option<u32>,
@ -480,7 +481,7 @@ impl PluginPlaylistSender {
pub async fn create_playlist( pub async fn create_playlist(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
user_id: String, user_id: String,
name: String, name: String,
description: Option<String>, description: Option<String>,
@ -505,7 +506,7 @@ impl PluginPlaylistSender {
pub async fn update_playlist( pub async fn update_playlist(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
playlist_id: String, playlist_id: String,
name: Option<String>, name: Option<String>,
description: Option<String>, description: Option<String>,
@ -530,7 +531,7 @@ impl PluginPlaylistSender {
pub async fn delete_playlist( pub async fn delete_playlist(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
playlist_id: String, playlist_id: String,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
@ -547,7 +548,7 @@ impl PluginPlaylistSender {
pub async fn add_tracks( pub async fn add_tracks(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
playlist_id: String, playlist_id: String,
track_ids: Vec<String>, track_ids: Vec<String>,
position: Option<u32>, position: Option<u32>,
@ -568,7 +569,7 @@ impl PluginPlaylistSender {
pub async fn remove_tracks( pub async fn remove_tracks(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
playlist_id: String, playlist_id: String,
track_ids: Vec<String>, track_ids: Vec<String>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@ -585,7 +586,7 @@ impl PluginPlaylistSender {
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o) rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
} }
pub async fn save(&self, mpsc_tx: OpaqueSender, playlist_id: String) -> anyhow::Result<()> { pub async fn save(&self, mpsc_tx: &OpaqueSender, playlist_id: String) -> anyhow::Result<()> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
mpsc_tx mpsc_tx
.sender .sender
@ -598,7 +599,7 @@ impl PluginPlaylistSender {
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o) rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
} }
pub async fn unsave(&self, mpsc_tx: OpaqueSender, playlist_id: String) -> anyhow::Result<()> { pub async fn unsave(&self, mpsc_tx: &OpaqueSender, playlist_id: String) -> anyhow::Result<()> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
mpsc_tx mpsc_tx
.sender .sender
@ -621,7 +622,7 @@ impl PluginSearchSender {
Self {} Self {}
} }
pub async fn chips(&self, mpsc_tx: OpaqueSender) -> anyhow::Result<Vec<String>> { pub async fn chips(&self, mpsc_tx: &OpaqueSender) -> anyhow::Result<Vec<String>> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
mpsc_tx mpsc_tx
.sender .sender
@ -635,7 +636,7 @@ impl PluginSearchSender {
pub async fn all( pub async fn all(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
query: String, query: String,
) -> anyhow::Result<SpotubeSearchResponseObject> { ) -> anyhow::Result<SpotubeSearchResponseObject> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
@ -652,7 +653,7 @@ impl PluginSearchSender {
pub async fn tracks( pub async fn tracks(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
query: String, query: String,
offset: Option<u32>, offset: Option<u32>,
limit: Option<u32>, limit: Option<u32>,
@ -673,7 +674,7 @@ impl PluginSearchSender {
pub async fn albums( pub async fn albums(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
query: String, query: String,
offset: Option<u32>, offset: Option<u32>,
limit: Option<u32>, limit: Option<u32>,
@ -694,7 +695,7 @@ impl PluginSearchSender {
pub async fn artists( pub async fn artists(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
query: String, query: String,
offset: Option<u32>, offset: Option<u32>,
limit: Option<u32>, limit: Option<u32>,
@ -715,7 +716,7 @@ impl PluginSearchSender {
pub async fn playlists( pub async fn playlists(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
query: String, query: String,
offset: Option<u32>, offset: Option<u32>,
limit: Option<u32>, limit: Option<u32>,
@ -746,7 +747,7 @@ impl PluginTrackSender {
pub async fn get_track( pub async fn get_track(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
id: String, id: String,
) -> anyhow::Result<SpotubeTrackObject> { ) -> anyhow::Result<SpotubeTrackObject> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
@ -761,7 +762,7 @@ impl PluginTrackSender {
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o) rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
} }
pub async fn save(&self, mpsc_tx: OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> { pub async fn save(&self, mpsc_tx: &OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
mpsc_tx mpsc_tx
.sender .sender
@ -774,7 +775,7 @@ impl PluginTrackSender {
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o) rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
} }
pub async fn unsave(&self, mpsc_tx: OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> { pub async fn unsave(&self, mpsc_tx: &OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
mpsc_tx mpsc_tx
.sender .sender
@ -789,7 +790,7 @@ impl PluginTrackSender {
pub async fn radio( pub async fn radio(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
id: String, id: String,
) -> anyhow::Result<Vec<SpotubeTrackObject>> { ) -> anyhow::Result<Vec<SpotubeTrackObject>> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
@ -814,7 +815,7 @@ impl PluginUserSender {
Self {} Self {}
} }
pub async fn me(&self, mpsc_tx: OpaqueSender) -> anyhow::Result<SpotubeUserObject> { pub async fn me(&self, mpsc_tx: &OpaqueSender) -> anyhow::Result<SpotubeUserObject> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
mpsc_tx mpsc_tx
.sender .sender
@ -826,7 +827,7 @@ impl PluginUserSender {
pub async fn saved_tracks( pub async fn saved_tracks(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
offset: Option<u32>, offset: Option<u32>,
limit: Option<u32>, limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> { ) -> anyhow::Result<SpotubePaginationResponseObject> {
@ -845,7 +846,7 @@ impl PluginUserSender {
pub async fn saved_albums( pub async fn saved_albums(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
offset: Option<u32>, offset: Option<u32>,
limit: Option<u32>, limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> { ) -> anyhow::Result<SpotubePaginationResponseObject> {
@ -864,7 +865,7 @@ impl PluginUserSender {
pub async fn saved_artists( pub async fn saved_artists(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
offset: Option<u32>, offset: Option<u32>,
limit: Option<u32>, limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> { ) -> anyhow::Result<SpotubePaginationResponseObject> {
@ -883,7 +884,7 @@ impl PluginUserSender {
pub async fn saved_playlists( pub async fn saved_playlists(
&self, &self,
mpsc_tx: OpaqueSender, mpsc_tx: &OpaqueSender,
offset: Option<u32>, offset: Option<u32>,
limit: Option<u32>, limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> { ) -> anyhow::Result<SpotubePaginationResponseObject> {

File diff suppressed because it is too large Load Diff

View File

@ -102,8 +102,8 @@ async fn plugin() -> anyhow::Result<()> {
}; };
let sender = plugin.create_context(PLUGIN_JS.to_string(), config.clone())?; let sender = plugin.create_context(PLUGIN_JS.to_string(), config.clone())?;
let (r1, r2) = tokio::join!( let (r1, r2) = tokio::join!(
plugin.core.check_update(sender.clone(), config.clone()), plugin.core.check_update(&sender, config.clone()),
plugin.core.check_update(sender.clone(), config.clone()) plugin.core.check_update(&sender, config.clone())
); );
r1?; r1?;
r2?; r2?;