mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-06 07:29:42 +00:00
Compare commits
No commits in common. "fe83f50286a57669d76a1142be9b9042825265b9" and "e6cdce4a149233365428108840ac9cc62eceea5b" have entirely different histories.
fe83f50286
...
e6cdce4a14
@ -58,6 +58,28 @@ 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();
|
||||||
@ -99,6 +121,25 @@ Future<void> main(List<String> rawArgs) async {
|
|||||||
|
|
||||||
await RustLib.init();
|
await RustLib.init();
|
||||||
|
|
||||||
|
final plugin = SpotubePlugin();
|
||||||
|
const config = PluginConfiguration(
|
||||||
|
entryPoint: "TestingPlugin",
|
||||||
|
abilities: [PluginAbility.metadata],
|
||||||
|
apis: [],
|
||||||
|
author: "KRTirtho",
|
||||||
|
description: "Testing Plugin",
|
||||||
|
name: "Testing Plugin",
|
||||||
|
pluginApiVersion: "2.0.0",
|
||||||
|
repository: null,
|
||||||
|
version: "0.1.0",
|
||||||
|
);
|
||||||
|
final sender = SpotubePlugin.newContext(
|
||||||
|
pluginScript: pluginJS,
|
||||||
|
pluginConfig: config,
|
||||||
|
);
|
||||||
|
|
||||||
|
await plugin.core.checkUpdate(mpscTx: sender, pluginConfig: config);
|
||||||
|
|
||||||
if (kIsDesktop) {
|
if (kIsDesktop) {
|
||||||
await windowManager.setPreventClose(true);
|
await windowManager.setPreventClose(true);
|
||||||
await YtDlp.instance
|
await YtDlp.instance
|
||||||
@ -178,58 +219,6 @@ class Spotube extends HookConsumerWidget {
|
|||||||
HomeWidget.registerInteractivityCallback(glanceBackgroundCallback);
|
HomeWidget.registerInteractivityCallback(glanceBackgroundCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
start() async {
|
|
||||||
final server = await ref.read(serverProvider.future);
|
|
||||||
|
|
||||||
final plugin = SpotubePlugin();
|
|
||||||
const pluginConfiguration = PluginConfiguration(
|
|
||||||
name: "Spotube Plugin",
|
|
||||||
description: "Spotube Plugin",
|
|
||||||
version: "1.0.0",
|
|
||||||
author: "Spotube",
|
|
||||||
entryPoint: "Plugin",
|
|
||||||
pluginApiVersion: "2.0.0",
|
|
||||||
apis: [PluginApi.localstorage, PluginApi.webview],
|
|
||||||
abilities: [PluginAbility.metadata],
|
|
||||||
);
|
|
||||||
final pluginContext = plugin.createContext(
|
|
||||||
serverEndpointUrl:
|
|
||||||
"http://${server.server.address.host}:${server.port}",
|
|
||||||
serverSecret: ref.read(serverRandomSecretProvider),
|
|
||||||
pluginScript: """
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await webview.open();
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class Plugin {
|
|
||||||
constructor() {
|
|
||||||
this.auth = new AuthEndpoint();
|
|
||||||
this.core = new CoreEndpoint();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
pluginConfig: pluginConfiguration,
|
|
||||||
);
|
|
||||||
|
|
||||||
await plugin.core.checkUpdate(
|
|
||||||
mpscTx: pluginContext, pluginConfig: pluginConfiguration);
|
|
||||||
}
|
|
||||||
|
|
||||||
start();
|
|
||||||
|
|
||||||
return () {
|
return () {
|
||||||
/// For enabling hot reload for audio player
|
/// For enabling hot reload for audio player
|
||||||
if (!kDebugMode) return;
|
if (!kDebugMode) return;
|
||||||
|
|||||||
@ -3,23 +3,10 @@ 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/webview.dart';
|
|
||||||
|
|
||||||
Handler pluginApiAuthMiddleware(Handler handler) {
|
|
||||||
return (Request request) {
|
|
||||||
final apiKey = request.headers['X-Plugin-Secret'] ??
|
|
||||||
request.url.queryParameters['secret'];
|
|
||||||
if (apiKey == null || apiKey != request.context['plugin_api_secret']) {
|
|
||||||
return Response.forbidden('Forbidden');
|
|
||||||
}
|
|
||||||
return handler(request);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
final serverRouterProvider = Provider((ref) {
|
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 router = Router();
|
final router = Router();
|
||||||
|
|
||||||
@ -32,32 +19,7 @@ final serverRouterProvider = Provider((ref) {
|
|||||||
router.get("/playback/previous", playbackRoutes.previousTrack);
|
router.get("/playback/previous", playbackRoutes.previousTrack);
|
||||||
router.get("/playback/next", playbackRoutes.nextTrack);
|
router.get("/playback/next", playbackRoutes.nextTrack);
|
||||||
|
|
||||||
router.post(
|
|
||||||
"/plugin-api/webview/create",
|
|
||||||
pluginApiAuthMiddleware(webviewRoutes.postCreateWebview),
|
|
||||||
);
|
|
||||||
router.get(
|
|
||||||
"/plugin-api/webview/<uid>/on-url-request",
|
|
||||||
pluginApiAuthMiddleware(webviewRoutes.getOnUrlRequestStream),
|
|
||||||
);
|
|
||||||
router.post(
|
|
||||||
"/plugin-api/webview/open",
|
|
||||||
pluginApiAuthMiddleware(webviewRoutes.postOpenWebview),
|
|
||||||
);
|
|
||||||
router.post(
|
|
||||||
"/plugin-api/webview/close",
|
|
||||||
pluginApiAuthMiddleware(webviewRoutes.postCloseWebview),
|
|
||||||
);
|
|
||||||
router.post(
|
|
||||||
"/plugin-api/webview/cookies",
|
|
||||||
pluginApiAuthMiddleware(webviewRoutes.postGetWebviewCookies),
|
|
||||||
);
|
|
||||||
|
|
||||||
router.all("/ws", connectRoutes.websocket);
|
router.all("/ws", connectRoutes.websocket);
|
||||||
|
|
||||||
ref.onDispose(() {
|
|
||||||
webviewRoutes.dispose();
|
|
||||||
});
|
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,134 +0,0 @@
|
|||||||
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<String, Webview> _webviews = {};
|
|
||||||
|
|
||||||
Future<Response> 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<Response> 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<List<int>>();
|
|
||||||
|
|
||||||
// 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<Response> 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<Response> 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<Response> 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<void> dispose() async {
|
|
||||||
for (final webview in _webviews.values) {
|
|
||||||
await webview.close();
|
|
||||||
}
|
|
||||||
_webviews.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final serverWebviewRoutesProvider = Provider((ref) => ServerWebviewRoutes());
|
|
||||||
@ -1,9 +1,7 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shelf/shelf.dart';
|
|
||||||
import 'package:shelf/shelf_io.dart';
|
import 'package:shelf/shelf_io.dart';
|
||||||
import 'package:spotube/provider/server/pipeline.dart';
|
import 'package:spotube/provider/server/pipeline.dart';
|
||||||
import 'package:spotube/provider/server/router.dart';
|
import 'package:spotube/provider/server/router.dart';
|
||||||
@ -11,16 +9,8 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart
|
|||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
|
|
||||||
final serverRandomSecretProvider = Provider<String>(
|
|
||||||
(ref) {
|
|
||||||
final random = Random.secure();
|
|
||||||
final values = List<int>.generate(16, (i) => random.nextInt(256));
|
|
||||||
return base64Url.encode(values);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final serverProvider = FutureProvider(
|
final serverProvider = FutureProvider(
|
||||||
(ref) async {
|
(ref) async {
|
||||||
final randomSecret = ref.watch(serverRandomSecretProvider);
|
|
||||||
final enabledRemoteConnect = ref.watch(
|
final enabledRemoteConnect = ref.watch(
|
||||||
userPreferencesProvider.select((value) => value.enableConnect),
|
userPreferencesProvider.select((value) => value.enableConnect),
|
||||||
);
|
);
|
||||||
@ -41,21 +31,8 @@ final serverProvider = FutureProvider(
|
|||||||
SpotubeMedia.serverPort = connectPort;
|
SpotubeMedia.serverPort = connectPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
final handler = pipeline.addMiddleware(logRequests()).addMiddleware(
|
|
||||||
(innerHandler) {
|
|
||||||
return (request) {
|
|
||||||
final updatedRequest = request.change(
|
|
||||||
context: {
|
|
||||||
'plugin_api_secret': randomSecret,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return innerHandler(updatedRequest);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
).addHandler(router.call);
|
|
||||||
|
|
||||||
final server = await serve(
|
final server = await serve(
|
||||||
handler,
|
pipeline.addHandler(router.call),
|
||||||
enabledRemoteConnect
|
enabledRemoteConnect
|
||||||
? InternetAddress.anyIPv4
|
? InternetAddress.anyIPv4
|
||||||
: InternetAddress.loopbackIPv4,
|
: InternetAddress.loopbackIPv4,
|
||||||
|
|||||||
@ -1,118 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
// 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 function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_receiver_is_total_eq`, `clone`, `clone`, `cmp`, `eq`, `fmt`, `fmt`, `partial_cmp`
|
|
||||||
|
|
||||||
class AuthEventObject {
|
|
||||||
final AuthEventType eventType;
|
|
||||||
|
|
||||||
const AuthEventObject({
|
|
||||||
required this.eventType,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => eventType.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is AuthEventObject &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
eventType == other.eventType;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AuthEventType {
|
|
||||||
login,
|
|
||||||
refreshSession,
|
|
||||||
logout,
|
|
||||||
;
|
|
||||||
}
|
|
||||||
@ -5,12 +5,11 @@
|
|||||||
|
|
||||||
import '../../frb_generated.dart';
|
import '../../frb_generated.dart';
|
||||||
import '../../lib.dart';
|
import '../../lib.dart';
|
||||||
import 'models/auth.dart';
|
|
||||||
import 'models/core.dart';
|
import 'models/core.dart';
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
import 'senders.dart';
|
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`: `console_log`, `js_executor_thread`, `register_globals`, `set_timeout`
|
||||||
// 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`
|
||||||
|
|
||||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<OpaqueSender>>
|
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<OpaqueSender>>
|
||||||
@ -20,58 +19,69 @@ abstract class OpaqueSender implements RustOpaqueInterface {
|
|||||||
set sender(SenderPluginCommand sender);
|
set sender(SenderPluginCommand sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<SpotubePlugin>>
|
class SpotubePlugin {
|
||||||
abstract class SpotubePlugin implements RustOpaqueInterface {
|
final PluginArtistSender artist;
|
||||||
Stream<AuthEventObject> authState();
|
final PluginAlbumSender album;
|
||||||
|
final PluginAudioSourceSender audioSource;
|
||||||
|
final PluginAuthSender auth;
|
||||||
|
final PluginBrowseSender browse;
|
||||||
|
final PluginCoreSender core;
|
||||||
|
final PluginPlaylistSender playlist;
|
||||||
|
final PluginSearchSender search;
|
||||||
|
final PluginTrackSender track;
|
||||||
|
final PluginUserSender user;
|
||||||
|
|
||||||
PluginAlbumSender get album;
|
const SpotubePlugin.raw({
|
||||||
|
required this.artist,
|
||||||
|
required this.album,
|
||||||
|
required this.audioSource,
|
||||||
|
required this.auth,
|
||||||
|
required this.browse,
|
||||||
|
required this.core,
|
||||||
|
required this.playlist,
|
||||||
|
required this.search,
|
||||||
|
required this.track,
|
||||||
|
required this.user,
|
||||||
|
});
|
||||||
|
|
||||||
PluginArtistSender get artist;
|
Future<void> dispose({required OpaqueSender tx}) => RustLib.instance.api
|
||||||
|
.crateApiPluginPluginSpotubePluginDispose(that: this, tx: tx);
|
||||||
PluginAudioSourceSender get audioSource;
|
|
||||||
|
|
||||||
PluginAuthSender get auth;
|
|
||||||
|
|
||||||
PluginBrowseSender get browse;
|
|
||||||
|
|
||||||
PluginCoreSender get core;
|
|
||||||
|
|
||||||
PluginPlaylistSender get playlist;
|
|
||||||
|
|
||||||
PluginSearchSender get search;
|
|
||||||
|
|
||||||
PluginTrackSender get track;
|
|
||||||
|
|
||||||
PluginUserSender get user;
|
|
||||||
|
|
||||||
set album(PluginAlbumSender album);
|
|
||||||
|
|
||||||
set artist(PluginArtistSender artist);
|
|
||||||
|
|
||||||
set audioSource(PluginAudioSourceSender audioSource);
|
|
||||||
|
|
||||||
set auth(PluginAuthSender auth);
|
|
||||||
|
|
||||||
set browse(PluginBrowseSender browse);
|
|
||||||
|
|
||||||
set core(PluginCoreSender core);
|
|
||||||
|
|
||||||
set playlist(PluginPlaylistSender playlist);
|
|
||||||
|
|
||||||
set search(PluginSearchSender search);
|
|
||||||
|
|
||||||
set track(PluginTrackSender track);
|
|
||||||
|
|
||||||
set user(PluginUserSender user);
|
|
||||||
|
|
||||||
Future<void> close({required OpaqueSender tx});
|
|
||||||
|
|
||||||
OpaqueSender createContext(
|
|
||||||
{required String pluginScript,
|
|
||||||
required PluginConfiguration pluginConfig,
|
|
||||||
required String serverEndpointUrl,
|
|
||||||
required String serverSecret});
|
|
||||||
|
|
||||||
factory SpotubePlugin() =>
|
factory SpotubePlugin() =>
|
||||||
RustLib.instance.api.crateApiPluginPluginSpotubePluginNew();
|
RustLib.instance.api.crateApiPluginPluginSpotubePluginNew();
|
||||||
|
|
||||||
|
static OpaqueSender newContext(
|
||||||
|
{required String pluginScript,
|
||||||
|
required PluginConfiguration pluginConfig}) =>
|
||||||
|
RustLib.instance.api.crateApiPluginPluginSpotubePluginNewContext(
|
||||||
|
pluginScript: pluginScript, pluginConfig: pluginConfig);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
artist.hashCode ^
|
||||||
|
album.hashCode ^
|
||||||
|
audioSource.hashCode ^
|
||||||
|
auth.hashCode ^
|
||||||
|
browse.hashCode ^
|
||||||
|
core.hashCode ^
|
||||||
|
playlist.hashCode ^
|
||||||
|
search.hashCode ^
|
||||||
|
track.hashCode ^
|
||||||
|
user.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is SpotubePlugin &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
artist == other.artist &&
|
||||||
|
album == other.album &&
|
||||||
|
audioSource == other.audioSource &&
|
||||||
|
auth == other.auth &&
|
||||||
|
browse == other.browse &&
|
||||||
|
core == other.core &&
|
||||||
|
playlist == other.playlist &&
|
||||||
|
search == other.search &&
|
||||||
|
track == other.track &&
|
||||||
|
user == other.user;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,6 @@ import 'models/user.dart';
|
|||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
import 'plugin.dart';
|
import 'plugin.dart';
|
||||||
|
|
||||||
// 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`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`
|
|
||||||
// These functions are ignored (category: IgnoreBecauseExplicitAttribute): `new`, `new`, `new`, `new`, `new`, `new`, `new`, `new`, `new`, `new`
|
// These functions are ignored (category: IgnoreBecauseExplicitAttribute): `new`, `new`, `new`, `new`, `new`, `new`, `new`, `new`, `new`, `new`
|
||||||
|
|
||||||
class PluginAlbumSender {
|
class PluginAlbumSender {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,6 @@ 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';
|
||||||
import 'api/plugin/models/audio_source.dart';
|
import 'api/plugin/models/audio_source.dart';
|
||||||
import 'api/plugin/models/auth.dart';
|
|
||||||
import 'api/plugin/models/browse.dart';
|
import 'api/plugin/models/browse.dart';
|
||||||
import 'api/plugin/models/core.dart';
|
import 'api/plugin/models/core.dart';
|
||||||
import 'api/plugin/models/image.dart';
|
import 'api/plugin/models/image.dart';
|
||||||
@ -45,10 +44,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
get rust_arc_decrement_strong_count_SenderPluginCommandPtr => wire
|
get rust_arc_decrement_strong_count_SenderPluginCommandPtr => wire
|
||||||
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSenderPluginCommandPtr;
|
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSenderPluginCommandPtr;
|
||||||
|
|
||||||
CrossPlatformFinalizerArg
|
|
||||||
get rust_arc_decrement_strong_count_SpotubePluginPtr => wire
|
|
||||||
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePluginPtr;
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
AnyhowException dco_decode_AnyhowException(dynamic raw);
|
AnyhowException dco_decode_AnyhowException(dynamic raw);
|
||||||
|
|
||||||
@ -67,31 +62,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSenderPluginCommand(
|
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSenderPluginCommand(
|
||||||
dynamic raw);
|
dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
SpotubePlugin
|
|
||||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
|
|
||||||
dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
OpaqueSender
|
OpaqueSender
|
||||||
dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
|
dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
|
||||||
dynamic raw);
|
dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
SpotubePlugin
|
|
||||||
dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
|
|
||||||
dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
OpaqueSender
|
OpaqueSender
|
||||||
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
|
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
|
||||||
dynamic raw);
|
dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
SpotubePlugin
|
|
||||||
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
|
|
||||||
dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
OpaqueSender
|
OpaqueSender
|
||||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
|
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
|
||||||
@ -107,24 +87,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSenderPluginCommand(
|
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSenderPluginCommand(
|
||||||
dynamic raw);
|
dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
SpotubePlugin
|
|
||||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
|
|
||||||
dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
RustStreamSink<AuthEventObject> dco_decode_StreamSink_auth_event_object_Sse(
|
|
||||||
dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String dco_decode_String(dynamic raw);
|
String dco_decode_String(dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
AuthEventObject dco_decode_auth_event_object(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
AuthEventType dco_decode_auth_event_type(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
bool dco_decode_bool(dynamic raw);
|
bool dco_decode_bool(dynamic raw);
|
||||||
|
|
||||||
@ -215,6 +180,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
SpotubeFullPlaylistObject dco_decode_box_autoadd_spotube_full_playlist_object(
|
SpotubeFullPlaylistObject dco_decode_box_autoadd_spotube_full_playlist_object(
|
||||||
dynamic raw);
|
dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SpotubePlugin dco_decode_box_autoadd_spotube_plugin(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
SpotubeSimpleAlbumObject dco_decode_box_autoadd_spotube_simple_album_object(
|
SpotubeSimpleAlbumObject dco_decode_box_autoadd_spotube_simple_album_object(
|
||||||
dynamic raw);
|
dynamic raw);
|
||||||
@ -446,6 +414,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
SpotubePaginationResponseObjectItem
|
SpotubePaginationResponseObjectItem
|
||||||
dco_decode_spotube_pagination_response_object_item(dynamic raw);
|
dco_decode_spotube_pagination_response_object_item(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SpotubePlugin dco_decode_spotube_plugin(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
SpotubeSearchResponseObject dco_decode_spotube_search_response_object(
|
SpotubeSearchResponseObject dco_decode_spotube_search_response_object(
|
||||||
dynamic raw);
|
dynamic raw);
|
||||||
@ -500,31 +471,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSenderPluginCommand(
|
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSenderPluginCommand(
|
||||||
SseDeserializer deserializer);
|
SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
SpotubePlugin
|
|
||||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
|
|
||||||
SseDeserializer deserializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
OpaqueSender
|
OpaqueSender
|
||||||
sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
|
sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
|
||||||
SseDeserializer deserializer);
|
SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
SpotubePlugin
|
|
||||||
sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
|
|
||||||
SseDeserializer deserializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
OpaqueSender
|
OpaqueSender
|
||||||
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
|
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
|
||||||
SseDeserializer deserializer);
|
SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
SpotubePlugin
|
|
||||||
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
|
|
||||||
SseDeserializer deserializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
OpaqueSender
|
OpaqueSender
|
||||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
|
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
|
||||||
@ -540,24 +496,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSenderPluginCommand(
|
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSenderPluginCommand(
|
||||||
SseDeserializer deserializer);
|
SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
SpotubePlugin
|
|
||||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
|
|
||||||
SseDeserializer deserializer);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
RustStreamSink<AuthEventObject> sse_decode_StreamSink_auth_event_object_Sse(
|
|
||||||
SseDeserializer deserializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String sse_decode_String(SseDeserializer deserializer);
|
String sse_decode_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
AuthEventObject sse_decode_auth_event_object(SseDeserializer deserializer);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
AuthEventType sse_decode_auth_event_type(SseDeserializer deserializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
bool sse_decode_bool(SseDeserializer deserializer);
|
bool sse_decode_bool(SseDeserializer deserializer);
|
||||||
|
|
||||||
@ -662,6 +603,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
SpotubeFullPlaylistObject sse_decode_box_autoadd_spotube_full_playlist_object(
|
SpotubeFullPlaylistObject sse_decode_box_autoadd_spotube_full_playlist_object(
|
||||||
SseDeserializer deserializer);
|
SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SpotubePlugin sse_decode_box_autoadd_spotube_plugin(
|
||||||
|
SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
SpotubeSimpleAlbumObject sse_decode_box_autoadd_spotube_simple_album_object(
|
SpotubeSimpleAlbumObject sse_decode_box_autoadd_spotube_simple_album_object(
|
||||||
SseDeserializer deserializer);
|
SseDeserializer deserializer);
|
||||||
@ -925,6 +870,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
sse_decode_spotube_pagination_response_object_item(
|
sse_decode_spotube_pagination_response_object_item(
|
||||||
SseDeserializer deserializer);
|
SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SpotubePlugin sse_decode_spotube_plugin(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
SpotubeSearchResponseObject sse_decode_spotube_search_response_object(
|
SpotubeSearchResponseObject sse_decode_spotube_search_response_object(
|
||||||
SseDeserializer deserializer);
|
SseDeserializer deserializer);
|
||||||
@ -983,31 +931,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSenderPluginCommand(
|
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSenderPluginCommand(
|
||||||
SenderPluginCommand self, SseSerializer serializer);
|
SenderPluginCommand self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
|
|
||||||
SpotubePlugin self, SseSerializer serializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void
|
void
|
||||||
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
|
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
|
||||||
OpaqueSender self, SseSerializer serializer);
|
OpaqueSender self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
|
|
||||||
SpotubePlugin self, SseSerializer serializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void
|
void
|
||||||
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
|
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
|
||||||
OpaqueSender self, SseSerializer serializer);
|
OpaqueSender self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
|
|
||||||
SpotubePlugin self, SseSerializer serializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void
|
void
|
||||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
|
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOpaqueSender(
|
||||||
@ -1023,25 +956,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSenderPluginCommand(
|
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSenderPluginCommand(
|
||||||
SenderPluginCommand self, SseSerializer serializer);
|
SenderPluginCommand self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
|
|
||||||
SpotubePlugin self, SseSerializer serializer);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_StreamSink_auth_event_object_Sse(
|
|
||||||
RustStreamSink<AuthEventObject> self, SseSerializer serializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_String(String self, SseSerializer serializer);
|
void sse_encode_String(String self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_auth_event_object(
|
|
||||||
AuthEventObject self, SseSerializer serializer);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_auth_event_type(AuthEventType self, SseSerializer serializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_bool(bool self, SseSerializer serializer);
|
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||||
|
|
||||||
@ -1142,6 +1059,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
void sse_encode_box_autoadd_spotube_full_playlist_object(
|
void sse_encode_box_autoadd_spotube_full_playlist_object(
|
||||||
SpotubeFullPlaylistObject self, SseSerializer serializer);
|
SpotubeFullPlaylistObject self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_box_autoadd_spotube_plugin(
|
||||||
|
SpotubePlugin self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_spotube_simple_album_object(
|
void sse_encode_box_autoadd_spotube_simple_album_object(
|
||||||
SpotubeSimpleAlbumObject self, SseSerializer serializer);
|
SpotubeSimpleAlbumObject self, SseSerializer serializer);
|
||||||
@ -1402,6 +1323,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
void sse_encode_spotube_pagination_response_object_item(
|
void sse_encode_spotube_pagination_response_object_item(
|
||||||
SpotubePaginationResponseObjectItem self, SseSerializer serializer);
|
SpotubePaginationResponseObjectItem self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_spotube_plugin(SpotubePlugin self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_spotube_search_response_object(
|
void sse_encode_spotube_search_response_object(
|
||||||
SpotubeSearchResponseObject self, SseSerializer serializer);
|
SpotubeSearchResponseObject self, SseSerializer serializer);
|
||||||
@ -1551,36 +1475,4 @@ class RustLibWire implements BaseWire {
|
|||||||
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSenderPluginCommand =
|
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSenderPluginCommand =
|
||||||
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSenderPluginCommandPtr
|
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSenderPluginCommandPtr
|
||||||
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
|
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
|
||||||
|
|
||||||
void
|
|
||||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
|
|
||||||
ffi.Pointer<ffi.Void> ptr,
|
|
||||||
) {
|
|
||||||
return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
|
|
||||||
ptr,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePluginPtr =
|
|
||||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
|
|
||||||
'frbgen_spotube_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin');
|
|
||||||
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin =
|
|
||||||
_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePluginPtr
|
|
||||||
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
|
|
||||||
|
|
||||||
void
|
|
||||||
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
|
|
||||||
ffi.Pointer<ffi.Void> ptr,
|
|
||||||
) {
|
|
||||||
return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
|
|
||||||
ptr,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePluginPtr =
|
|
||||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
|
|
||||||
'frbgen_spotube_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin');
|
|
||||||
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin =
|
|
||||||
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePluginPtr
|
|
||||||
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -742,7 +742,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
fk_user_agent:
|
fk_user_agent:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
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: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: random_user_agents
|
name: random_user_agents
|
||||||
sha256: "80dc025723a73f04797351aa6ef2fddb14836f86a752711a2f8a04e37c4ccdff"
|
sha256: "95647149687167e82a7b39e1b4616fdebb574981b71b6f0cfca21b69f36293a8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.18"
|
version: "1.0.17"
|
||||||
recase:
|
recase:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -42,10 +42,6 @@ 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
|
||||||
@ -161,7 +157,6 @@ 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
|
||||||
|
|||||||
813
rust/Cargo.lock
generated
813
rust/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -15,17 +15,13 @@ 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", "macro", "classes", "bindgen"] }
|
rquickjs = { version = "0", features = ["chrono", "futures"] }
|
||||||
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"] }
|
||||||
eventsource-client = "0.15.1"
|
|
||||||
reqwest = { version = "0.12", features = ["json"] }
|
|
||||||
|
|
||||||
[patch."https://github.com/DelSkayn/rquickjs"]
|
[patch."https://github.com/DelSkayn/rquickjs"]
|
||||||
rquickjs = "0.10.0"
|
rquickjs = "0.10.0"
|
||||||
[path.crates-io]
|
|
||||||
rquickjs = "0.10.0"
|
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] }
|
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] }
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub enum AuthEventType {
|
|
||||||
Login,
|
|
||||||
RefreshSession,
|
|
||||||
Logout,
|
|
||||||
}
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct AuthEventObject {
|
|
||||||
pub event_type: AuthEventType,
|
|
||||||
}
|
|
||||||
@ -9,4 +9,3 @@ pub mod track;
|
|||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod pagination;
|
pub mod pagination;
|
||||||
pub mod core;
|
pub mod core;
|
||||||
pub mod auth;
|
|
||||||
|
|||||||
@ -3,26 +3,20 @@ 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,
|
||||||
execute_core, execute_playlist, execute_search, execute_track, execute_user,
|
execute_core, execute_playlist, execute_search, execute_track, execute_user,
|
||||||
};
|
};
|
||||||
use crate::api::plugin::models::auth::{AuthEventObject, AuthEventType};
|
|
||||||
use crate::api::plugin::models::core::PluginConfiguration;
|
use crate::api::plugin::models::core::PluginConfiguration;
|
||||||
use crate::api::plugin::senders::{
|
use crate::api::plugin::senders::{
|
||||||
PluginAlbumSender, PluginArtistSender, PluginAudioSourceSender, PluginAuthSender,
|
PluginAlbumSender, PluginArtistSender, PluginAudioSourceSender, PluginAuthSender,
|
||||||
PluginBrowseSender, PluginCoreSender, PluginPlaylistSender, PluginSearchSender,
|
PluginBrowseSender, PluginCoreSender, PluginPlaylistSender, PluginSearchSender,
|
||||||
PluginTrackSender, PluginUserSender,
|
PluginTrackSender, PluginUserSender,
|
||||||
};
|
};
|
||||||
use crate::frb_generated::StreamSink;
|
|
||||||
use crate::internal::apis::webview;
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use flutter_rust_bridge::{frb, Rust2DartSendError};
|
use flutter_rust_bridge::frb;
|
||||||
|
use llrt_modules::{abort, buffer, console, crypto, events, exceptions, fetch, navigator, timers, url, util};
|
||||||
use llrt_modules::module_builder::ModuleBuilder;
|
use llrt_modules::module_builder::ModuleBuilder;
|
||||||
use llrt_modules::{
|
use rquickjs::{async_with, AsyncContext, AsyncRuntime, Error};
|
||||||
abort, buffer, console, crypto, events, exceptions, fetch, navigator, timers, url, util,
|
|
||||||
};
|
|
||||||
use rquickjs::prelude::Func;
|
|
||||||
use rquickjs::{async_with, AsyncContext, AsyncRuntime, CatchResultExt, 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::Sender;
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
use tokio::task::LocalSet;
|
use tokio::task::LocalSet;
|
||||||
|
|
||||||
@ -31,11 +25,8 @@ pub struct OpaqueSender {
|
|||||||
pub sender: Sender<PluginCommand>,
|
pub sender: Sender<PluginCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[frb(ignore)]
|
#[frb(ignore)]
|
||||||
async fn create_context(
|
async fn create_context() -> anyhow::Result<(AsyncContext, AsyncRuntime)> {
|
||||||
server_endpoint_url: String,
|
|
||||||
server_secret: String,
|
|
||||||
) -> anyhow::Result<(AsyncContext, AsyncRuntime)> {
|
|
||||||
let runtime = AsyncRuntime::new().expect("Unable to create async runtime");
|
let runtime = AsyncRuntime::new().expect("Unable to create async runtime");
|
||||||
|
|
||||||
let mut module_builder = ModuleBuilder::new();
|
let mut module_builder = ModuleBuilder::new();
|
||||||
@ -51,7 +42,8 @@ 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)
|
||||||
|
;
|
||||||
|
|
||||||
let (module_resolver, module_loader, global_attachment) = module_builder.build();
|
let (module_resolver, module_loader, global_attachment) = module_builder.build();
|
||||||
runtime
|
runtime
|
||||||
@ -63,9 +55,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| {
|
||||||
global_attachment.attach(&ctx).catch(&ctx).map_err(|e| anyhow!("Failed to attach global modules: {}", e))?;
|
global_attachment.attach(&ctx)?;
|
||||||
webview::init(&ctx, server_endpoint_url, server_secret).catch(&ctx).map_err(|e| anyhow!("Failed to initialize WebView API: {}", e))?;
|
Ok::<(), Error>(())
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow!("Failed to register globals: {}", e))?;
|
.map_err(|e| anyhow!("Failed to register globals: {}", e))?;
|
||||||
@ -74,8 +65,8 @@ async fn create_context(
|
|||||||
}
|
}
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
async fn js_executor_thread(
|
async fn js_executor_thread(
|
||||||
rx: &mut Receiver<PluginCommand>,
|
rx: &mut mpsc::Receiver<PluginCommand>,
|
||||||
context: &AsyncContext,
|
ctx: &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);
|
||||||
@ -84,21 +75,19 @@ async fn js_executor_thread(
|
|||||||
return anyhow::Ok(());
|
return anyhow::Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let context = context.clone();
|
let ctx = ctx.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, &context).await,
|
PluginCommand::Artist(commands) => execute_artists(commands, &ctx).await,
|
||||||
PluginCommand::Album(commands) => execute_albums(commands, &context).await,
|
PluginCommand::Album(commands) => execute_albums(commands, &ctx).await,
|
||||||
PluginCommand::AudioSource(commands) => {
|
PluginCommand::AudioSource(commands) => execute_audio_source(commands, &ctx).await,
|
||||||
execute_audio_source(commands, &context).await
|
PluginCommand::Auth(commands) => execute_auth(commands, &ctx).await,
|
||||||
}
|
PluginCommand::Browse(commands) => execute_browse(commands, &ctx).await,
|
||||||
PluginCommand::Auth(commands) => execute_auth(commands, &context).await,
|
PluginCommand::Core(commands) => execute_core(commands, &ctx).await,
|
||||||
PluginCommand::Browse(commands) => execute_browse(commands, &context).await,
|
PluginCommand::Playlist(commands) => execute_playlist(commands, &ctx).await,
|
||||||
PluginCommand::Core(commands) => execute_core(commands, &context).await,
|
PluginCommand::Search(commands) => execute_search(commands, &ctx).await,
|
||||||
PluginCommand::Playlist(commands) => execute_playlist(commands, &context).await,
|
PluginCommand::Track(commands) => execute_track(commands, &ctx).await,
|
||||||
PluginCommand::Search(commands) => execute_search(commands, &context).await,
|
PluginCommand::User(commands) => execute_user(commands, &ctx).await,
|
||||||
PluginCommand::Track(commands) => execute_track(commands, &context).await,
|
|
||||||
PluginCommand::User(commands) => execute_user(commands, &context).await,
|
|
||||||
PluginCommand::Shutdown => unreachable!(),
|
PluginCommand::Shutdown => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -121,15 +110,11 @@ pub struct SpotubePlugin {
|
|||||||
pub search: PluginSearchSender,
|
pub search: PluginSearchSender,
|
||||||
pub track: PluginTrackSender,
|
pub track: PluginTrackSender,
|
||||||
pub user: PluginUserSender,
|
pub user: PluginUserSender,
|
||||||
event_tx: Sender<AuthEventObject>,
|
|
||||||
event_rx: Receiver<AuthEventObject>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpotubePlugin {
|
impl SpotubePlugin {
|
||||||
#[frb(sync)]
|
#[frb(sync)]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let (event_tx, event_rx) = mpsc::channel(32);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
artist: PluginArtistSender::new(),
|
artist: PluginArtistSender::new(),
|
||||||
album: PluginAlbumSender::new(),
|
album: PluginAlbumSender::new(),
|
||||||
@ -141,30 +126,16 @@ impl SpotubePlugin {
|
|||||||
search: PluginSearchSender::new(),
|
search: PluginSearchSender::new(),
|
||||||
track: PluginTrackSender::new(),
|
track: PluginTrackSender::new(),
|
||||||
user: PluginUserSender::new(),
|
user: PluginUserSender::new(),
|
||||||
event_tx,
|
|
||||||
event_rx,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn auth_state(&mut self, sink: StreamSink<AuthEventObject>) -> anyhow::Result<()> {
|
#[frb(sync)]
|
||||||
while let Some(event) = self.event_rx.recv().await {
|
pub fn new_context(
|
||||||
sink.add(event)
|
|
||||||
.map_err(|e: Rust2DartSendError| anyhow::anyhow!(e))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// #[frb(sync)]
|
|
||||||
pub fn create_context(
|
|
||||||
&self,
|
|
||||||
plugin_script: String,
|
plugin_script: String,
|
||||||
plugin_config: PluginConfiguration,
|
plugin_config: PluginConfiguration,
|
||||||
server_endpoint_url: String,
|
|
||||||
server_secret: String,
|
|
||||||
) -> anyhow::Result<OpaqueSender> {
|
) -> anyhow::Result<OpaqueSender> {
|
||||||
let (command_tx, mut command_rx) = mpsc::channel(32);
|
let (command_tx, mut command_rx) = mpsc::channel(32);
|
||||||
let sender = self.event_tx.clone();
|
|
||||||
let _thread_handle = thread::spawn(move || {
|
let _thread_handle = thread::spawn(move || {
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
@ -172,10 +143,7 @@ impl SpotubePlugin {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let local = LocalSet::new();
|
let local = LocalSet::new();
|
||||||
if let Err(e) = local.block_on(&rt, async {
|
if let Err(e) = local.block_on(&rt, async {
|
||||||
let (ctx, _) = create_context(
|
let (ctx, runtime) = create_context().await?;
|
||||||
server_endpoint_url,
|
|
||||||
server_secret,
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
let injection = format!(
|
let injection = format!(
|
||||||
"globalThis.pluginInstance = new {}();",
|
"globalThis.pluginInstance = new {}();",
|
||||||
@ -183,43 +151,7 @@ impl SpotubePlugin {
|
|||||||
);
|
);
|
||||||
let script = format!("{}\n{}", plugin_script, injection);
|
let script = format!("{}\n{}", plugin_script, injection);
|
||||||
|
|
||||||
async_with!(ctx => |cx| {
|
ctx.with(|cx| cx.eval::<(), _>(script.as_str())).await?;
|
||||||
cx.eval::<(), _>(script.as_str())
|
|
||||||
.catch(&cx).map_err(|e| anyhow!("Failed to evaluate supplied plugin script: {}", e))
|
|
||||||
}).await?;
|
|
||||||
|
|
||||||
async_with!(ctx => |ctx|{
|
|
||||||
let globals = ctx.globals();
|
|
||||||
let callback = Func::new(move |event: Object| -> rquickjs::Result<()>{
|
|
||||||
let sender_clone = sender.clone();
|
|
||||||
let event_type_js: rquickjs::String = event.get("eventType")?;
|
|
||||||
let event_type = serde_json::from_value::<AuthEventType>(serde_json::Value::String(event_type_js.to_string()?));
|
|
||||||
if let Ok(event_type) = event_type {
|
|
||||||
tokio::spawn(async move{
|
|
||||||
if let Err(e) = sender_clone.send(AuthEventObject{event_type}).await {
|
|
||||||
eprintln!("Error sending auth event: {:?}", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(Error::FromJs{
|
|
||||||
from: "event.eventType",
|
|
||||||
to: "AuthEventType",
|
|
||||||
message: Some("Failed to deserialize eventType".to_string())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Err(e) = globals.get::<_, Object>("pluginInstance")?.get::<_, Object>("auth")?.set(
|
|
||||||
"onAuthEvent", callback
|
|
||||||
) {
|
|
||||||
eprintln!("Error setting auth event handler: {:?}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok::<(), Error>(())
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|e| anyhow!("[onAuthEvent] {e}"))?;
|
|
||||||
|
|
||||||
if let Err(e) = js_executor_thread(&mut command_rx, &ctx).await {
|
if let Err(e) = js_executor_thread(&mut command_rx, &ctx).await {
|
||||||
eprintln!("JS executor error: {}", e);
|
eprintln!("JS executor error: {}", e);
|
||||||
@ -233,7 +165,7 @@ impl SpotubePlugin {
|
|||||||
Ok(OpaqueSender { sender: command_tx })
|
Ok(OpaqueSender { sender: command_tx })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn close(&self, tx: OpaqueSender) -> anyhow::Result<()> {
|
pub async fn dispose(&self, tx: OpaqueSender) -> anyhow::Result<()> {
|
||||||
tx.sender.send(PluginCommand::Shutdown).await?;
|
tx.sender.send(PluginCommand::Shutdown).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
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,
|
||||||
@ -18,10 +19,8 @@ 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)]
|
|
||||||
pub struct PluginArtistSender {}
|
pub struct PluginArtistSender {}
|
||||||
|
|
||||||
impl PluginArtistSender {
|
impl PluginArtistSender {
|
||||||
@ -32,7 +31,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 +48,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 +69,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 +90,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 +109,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 +122,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
|
||||||
@ -137,7 +136,6 @@ impl PluginArtistSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct PluginAlbumSender {}
|
pub struct PluginAlbumSender {}
|
||||||
|
|
||||||
impl PluginAlbumSender {
|
impl PluginAlbumSender {
|
||||||
@ -148,7 +146,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 +163,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 +184,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 +201,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 +214,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
|
||||||
@ -230,7 +228,6 @@ impl PluginAlbumSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct PluginAudioSourceSender {}
|
pub struct PluginAudioSourceSender {}
|
||||||
|
|
||||||
impl PluginAudioSourceSender {
|
impl PluginAudioSourceSender {
|
||||||
@ -241,7 +238,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 +255,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();
|
||||||
@ -274,7 +271,6 @@ impl PluginAudioSourceSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct PluginAuthSender {}
|
pub struct PluginAuthSender {}
|
||||||
|
|
||||||
impl PluginAuthSender {
|
impl PluginAuthSender {
|
||||||
@ -283,7 +279,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 +291,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 +303,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
|
||||||
@ -320,7 +316,6 @@ impl PluginAuthSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct PluginBrowseSender {}
|
pub struct PluginBrowseSender {}
|
||||||
|
|
||||||
impl PluginBrowseSender {
|
impl PluginBrowseSender {
|
||||||
@ -331,7 +326,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 +345,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>,
|
||||||
@ -370,7 +365,6 @@ impl PluginBrowseSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct PluginCoreSender {}
|
pub struct PluginCoreSender {}
|
||||||
|
|
||||||
impl PluginCoreSender {
|
impl PluginCoreSender {
|
||||||
@ -381,9 +375,10 @@ 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
|
||||||
@ -393,16 +388,14 @@ impl PluginCoreSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await
|
rx.await.map_err(|e| {
|
||||||
.map_err(|e| {
|
eprintln!("RecvError: {}", e);
|
||||||
eprintln!("RecvError: {}", e);
|
eprintln!("Stack trace:\n{:?}", Backtrace::capture());
|
||||||
eprintln!("Stack trace:\n{:?}", Backtrace::capture());
|
anyhow!("{e}")
|
||||||
anyhow!("{e}")
|
}).and_then(|o| o)
|
||||||
})
|
|
||||||
.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
|
||||||
@ -416,7 +409,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();
|
||||||
@ -432,7 +425,6 @@ impl PluginCoreSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct PluginPlaylistSender {}
|
pub struct PluginPlaylistSender {}
|
||||||
|
|
||||||
impl PluginPlaylistSender {
|
impl PluginPlaylistSender {
|
||||||
@ -443,7 +435,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();
|
||||||
@ -460,7 +452,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>,
|
||||||
@ -481,7 +473,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>,
|
||||||
@ -506,7 +498,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>,
|
||||||
@ -531,7 +523,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();
|
||||||
@ -548,7 +540,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>,
|
||||||
@ -569,7 +561,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<()> {
|
||||||
@ -586,7 +578,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
|
||||||
@ -599,7 +591,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
|
||||||
@ -613,7 +605,6 @@ impl PluginPlaylistSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct PluginSearchSender {}
|
pub struct PluginSearchSender {}
|
||||||
|
|
||||||
impl PluginSearchSender {
|
impl PluginSearchSender {
|
||||||
@ -622,7 +613,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
|
||||||
@ -636,7 +627,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();
|
||||||
@ -653,7 +644,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>,
|
||||||
@ -674,7 +665,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>,
|
||||||
@ -695,7 +686,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>,
|
||||||
@ -716,7 +707,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>,
|
||||||
@ -736,7 +727,6 @@ impl PluginSearchSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct PluginTrackSender {}
|
pub struct PluginTrackSender {}
|
||||||
|
|
||||||
impl PluginTrackSender {
|
impl PluginTrackSender {
|
||||||
@ -747,7 +737,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();
|
||||||
@ -762,7 +752,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
|
||||||
@ -775,7 +765,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
|
||||||
@ -790,7 +780,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();
|
||||||
@ -806,7 +796,6 @@ impl PluginTrackSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct PluginUserSender {}
|
pub struct PluginUserSender {}
|
||||||
|
|
||||||
impl PluginUserSender {
|
impl PluginUserSender {
|
||||||
@ -815,7 +804,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
|
||||||
@ -827,7 +816,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> {
|
||||||
@ -846,7 +835,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> {
|
||||||
@ -865,7 +854,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> {
|
||||||
@ -884,7 +873,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
@ -1,167 +0,0 @@
|
|||||||
use eventsource_client::{ClientBuilder, Client, SSE};
|
|
||||||
use flutter_rust_bridge::for_generated::futures::StreamExt;
|
|
||||||
use rquickjs::function::Func;
|
|
||||||
use rquickjs::{CatchResultExt, Ctx, Error as JsError, Function, Object, Value};
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
|
|
||||||
fn connect_sse<'js>(ctx: Ctx<'js>, config: Object<'js>) -> rquickjs::Result<Object<'js>> {
|
|
||||||
let url: String = config.get("url")?;
|
|
||||||
let on_connecting: Function = config.get("onConnecting")?;
|
|
||||||
let on_open: Function = config.get("onOpen")?;
|
|
||||||
let on_message: Function = config.get("onMessage")?;
|
|
||||||
let on_error: Function = config.get("onError")?;
|
|
||||||
|
|
||||||
let (close_tx, mut close_rx) = mpsc::unbounded_channel::<()>();
|
|
||||||
|
|
||||||
if let Err(e) = on_connecting.call::<_, ()>(()).catch(&ctx) {
|
|
||||||
eprintln!("Error in onConnecting callback: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spawn the SSE background task using Ctx::spawn
|
|
||||||
let _ = ctx.clone().spawn(async move {
|
|
||||||
let client = ClientBuilder::for_url(&url);
|
|
||||||
if let Err(err) = client {
|
|
||||||
eprintln!("Error in ClientBuilder::for_url: {}", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let client = client.unwrap().build();
|
|
||||||
|
|
||||||
// Notify "open"
|
|
||||||
if let Err(e) = on_open.call::<(), ()>(()) {
|
|
||||||
eprintln!("Error in onOpen callback: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now listen to SSE events OR close signal
|
|
||||||
let mut stream = Box::pin(client.stream());
|
|
||||||
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
// Check for close signal first
|
|
||||||
_ = close_rx.recv() => {
|
|
||||||
// Close requested — drop stream and exit
|
|
||||||
drop(stream);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
event = stream.next() => {
|
|
||||||
match event {
|
|
||||||
Some(Ok(SSE::Event(msg))) => {
|
|
||||||
let data = msg.data.clone();
|
|
||||||
if let Err(e) = on_message.call::<_, ()>((data,)) {
|
|
||||||
eprintln!("Error in onMessage callback: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(Ok(SSE::Connected(details))) => {
|
|
||||||
println!("SSE Connected: {:?}", details);
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(Ok(SSE::Comment(comment))) => {
|
|
||||||
println!("SSE Comment: {}", comment);
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(Err(err)) => {
|
|
||||||
if let Err(e) = on_error.call::<_, ()>((err.to_string(),)) {
|
|
||||||
eprintln!("Error in onError callback: {}", e);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
None => {
|
|
||||||
println!("SSE Stream ended gracefully");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Create the close function that sends signal via channel
|
|
||||||
let close_fn = Function::new(ctx.clone(), move |_ctx: Ctx<'_>| {
|
|
||||||
// Send close signal — ignore errors if receiver is gone
|
|
||||||
let _ = close_tx.send(());
|
|
||||||
Ok::<(), JsError>(())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Return { close: () => void }
|
|
||||||
let result = Object::new(ctx)?;
|
|
||||||
result.set("close", close_fn)?;
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(ctx: &Ctx) -> rquickjs::Result<()> {
|
|
||||||
let globals = ctx.globals();
|
|
||||||
|
|
||||||
globals.set("__connectSSE", Func::new(connect_sse))?;
|
|
||||||
|
|
||||||
ctx.eval::<Value, _>(
|
|
||||||
r#"
|
|
||||||
globalThis.EventSource = class EventSource {
|
|
||||||
#listeners = {};
|
|
||||||
|
|
||||||
constructor(url, options) {
|
|
||||||
this.url = url;
|
|
||||||
this.options = options;
|
|
||||||
|
|
||||||
this.close = __connectSSE({
|
|
||||||
url: this.url,
|
|
||||||
onConnecting: this.#onConnecting.bind(this),
|
|
||||||
onOpen: this.#onOpen.bind(this),
|
|
||||||
onMessage: this.#onMessage.bind(this),
|
|
||||||
onError: this.#onError.bind(this),
|
|
||||||
}).close;
|
|
||||||
}
|
|
||||||
|
|
||||||
#onMessage(data) {
|
|
||||||
console.log("Received message:", data);
|
|
||||||
if (this.onmessage) {
|
|
||||||
this.onmessage(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventLines = data.split('\n');
|
|
||||||
|
|
||||||
if(eventLines.length === 0) return;
|
|
||||||
const eventNameChunks = eventLines[0].split("event:");
|
|
||||||
|
|
||||||
if(eventNameChunks.length === 0) return;
|
|
||||||
const eventName = eventNameChunks[1].trim();
|
|
||||||
|
|
||||||
if (!this.#listeners[eventName]) return;
|
|
||||||
const eventDataChunks = eventLines[1].split("data:");
|
|
||||||
|
|
||||||
if(eventDataChunks.length === 0) return;
|
|
||||||
const eventData = eventDataChunks[1].trim();
|
|
||||||
|
|
||||||
if (!eventData) return;
|
|
||||||
this.#listeners[eventName](eventData);
|
|
||||||
}
|
|
||||||
|
|
||||||
#onConnecting() {
|
|
||||||
this.readyState = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#onOpen() {
|
|
||||||
this.readyState = 1;
|
|
||||||
if (this.onopen) {
|
|
||||||
this.onopen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#onError(error) {
|
|
||||||
this.readyState = 2;
|
|
||||||
if (this.onerror) {
|
|
||||||
this.onerror(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addEventListener(event, callback) {
|
|
||||||
this.#listeners[event] ??= [];
|
|
||||||
this.#listeners[event].push(callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@ -1,2 +1 @@
|
|||||||
pub mod event_source;
|
pub mod fetcher;
|
||||||
pub mod webview;
|
|
||||||
|
|||||||
@ -1,215 +0,0 @@
|
|||||||
use eventsource_client::{Client as EventSourceClient, ClientBuilder};
|
|
||||||
use flutter_rust_bridge::for_generated::futures::StreamExt;
|
|
||||||
use rquickjs::{class::Trace, Class, Ctx, Function, JsLifetime};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
struct WebViewURLRequest {
|
|
||||||
url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
struct WebViewUIDRequest {
|
|
||||||
uid: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
struct WebViewCookiesRequest {
|
|
||||||
url: String,
|
|
||||||
uid: String,
|
|
||||||
}
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
struct WebViewResponse {
|
|
||||||
uid: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Trace, JsLifetime)]
|
|
||||||
#[rquickjs::class]
|
|
||||||
pub struct WebView<'js> {
|
|
||||||
pub url: String,
|
|
||||||
pub uid: String,
|
|
||||||
#[qjs(skip_trace)]
|
|
||||||
endpoint_url: String,
|
|
||||||
#[qjs(skip_trace)]
|
|
||||||
secret: String,
|
|
||||||
#[qjs(skip_trace)]
|
|
||||||
callbacks: Vec<Function<'js>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rquickjs::methods]
|
|
||||||
impl<'js> WebView<'js> {
|
|
||||||
#[qjs(constructor)]
|
|
||||||
pub fn new(url: String, uid: String, endpoint_url: String, secret: String) -> Self {
|
|
||||||
Self {
|
|
||||||
url,
|
|
||||||
uid,
|
|
||||||
endpoint_url,
|
|
||||||
secret,
|
|
||||||
callbacks: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[qjs(static)]
|
|
||||||
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 secret: String = ctx.globals().get("__webviewSecret").unwrap_or_default();
|
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let endpoint = format!("{}/plugin-api/webview/create", endpoint_url.clone());
|
|
||||||
|
|
||||||
let response = client
|
|
||||||
.post(&endpoint)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.header("X-Plugin-Secret", &secret)
|
|
||||||
.json(&WebViewURLRequest { url: url.clone() })
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
rquickjs::Error::new_from_js_message("reqwest", "Error", &e.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let data: WebViewResponse = response.json().await.map_err(|e| {
|
|
||||||
rquickjs::Error::new_from_js_message("reqwest", "Error", &e.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let webview = WebView::new(url, data.uid, endpoint_url, secret);
|
|
||||||
|
|
||||||
Class::instance(ctx, webview)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn open(&self) -> rquickjs::Result<()> {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let endpoint = format!("{}/plugin-api/webview/open", self.endpoint_url);
|
|
||||||
|
|
||||||
client
|
|
||||||
.post(&endpoint)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.header("X-Plugin-Secret", &self.secret)
|
|
||||||
.json(&WebViewUIDRequest {
|
|
||||||
uid: self.uid.clone(),
|
|
||||||
})
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
rquickjs::Error::new_from_js_message("reqwest", "Error", &e.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
self.url_change_task().await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn cookies(&self, ctx: Ctx<'js>) -> rquickjs::Result<rquickjs::Value<'js>> {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let endpoint = format!("{}/plugin-api/webview/cookies", self.endpoint_url);
|
|
||||||
|
|
||||||
let response = client
|
|
||||||
.post(&endpoint)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.header("X-Plugin-Secret", &self.secret)
|
|
||||||
.json(&WebViewCookiesRequest {
|
|
||||||
url: self.url.clone(),
|
|
||||||
uid: self.uid.clone(),
|
|
||||||
})
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
rquickjs::Error::new_from_js_message("reqwest", "Error", &e.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let data: serde_json::Value = response.json().await.map_err(|e| {
|
|
||||||
rquickjs::Error::new_from_js_message("reqwest", "Error", &e.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let value = ctx.json_parse(data.to_string())?;
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn close(&self) -> rquickjs::Result<()> {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let endpoint = format!("{}/plugin-api/webview/close", self.endpoint_url);
|
|
||||||
|
|
||||||
client
|
|
||||||
.post(&endpoint)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.header("X-Plugin-Secret", &self.secret)
|
|
||||||
.json(&WebViewUIDRequest {
|
|
||||||
uid: self.uid.clone(),
|
|
||||||
})
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
rquickjs::Error::new_from_js_message("reqwest", "Error", &e.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[qjs(rename = "onUrlChange")]
|
|
||||||
pub fn on_url_change(&mut self, callback: Function<'js>) -> rquickjs::Result<()> {
|
|
||||||
self.callbacks.push(callback);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn url_change_task(&self) {
|
|
||||||
let endpoint = format!(
|
|
||||||
"{}/plugin-api/webview/{}/on-url-request",
|
|
||||||
self.endpoint_url, self.uid
|
|
||||||
);
|
|
||||||
|
|
||||||
let secret = self.secret.clone();
|
|
||||||
|
|
||||||
let mut backoff = 1u64;
|
|
||||||
const MAX_BACKOFF: u64 = 60;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let client = ClientBuilder::for_url(&endpoint)
|
|
||||||
.expect("Failed to create EventSourceClient")
|
|
||||||
.header("X-Plugin-Secret", &secret)
|
|
||||||
.expect("Failed to set header for EventSourceClient")
|
|
||||||
.build();
|
|
||||||
let mut stream = client.stream();
|
|
||||||
while let Some(event) = stream.next().await {
|
|
||||||
match event {
|
|
||||||
Ok(eventsource_client::SSE::Event(msg)) => {
|
|
||||||
if msg.event_type != "url-request" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
backoff = 1;
|
|
||||||
if let Ok(data) = serde_json::from_str::<HashMap<String, String>>(&msg.data)
|
|
||||||
{
|
|
||||||
let url = data.get("url").cloned().unwrap_or_default();
|
|
||||||
for callback in self.callbacks.iter() {
|
|
||||||
if let Err(e) = callback.call::<_, ()>((url.clone(),)) {
|
|
||||||
eprintln!("Error in onUrlChange callback: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
eprintln!("Failed to parse event data: {}", msg.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("Error in EventSource stream: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eprintln!("EventSource disconnected. Reconnecting in {}s...", backoff);
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_secs(backoff)).await;
|
|
||||||
backoff = (backoff * 2).min(MAX_BACKOFF);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
Class::<WebView>::define(&ctx.globals())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@ -2,6 +2,7 @@ use crate::api::plugin::models::core::{
|
|||||||
PluginConfiguration, PluginUpdateAvailable, ScrobbleDetails,
|
PluginConfiguration, PluginUpdateAvailable, ScrobbleDetails,
|
||||||
};
|
};
|
||||||
use crate::internal::utils::{js_invoke_async_method_to_json, js_invoke_method_to_json};
|
use crate::internal::utils::{js_invoke_async_method_to_json, js_invoke_method_to_json};
|
||||||
|
use anyhow::anyhow;
|
||||||
use flutter_rust_bridge::frb;
|
use flutter_rust_bridge::frb;
|
||||||
use rquickjs::{async_with, AsyncContext};
|
use rquickjs::{async_with, AsyncContext};
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
pub mod album;
|
pub mod album;
|
||||||
|
pub mod apis;
|
||||||
pub mod artist;
|
pub mod artist;
|
||||||
pub mod audio_source;
|
pub mod audio_source;
|
||||||
pub mod browse;
|
pub mod browse;
|
||||||
@ -8,6 +9,5 @@ pub mod search;
|
|||||||
pub mod track;
|
pub mod track;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub(crate) mod utils;
|
mod utils;
|
||||||
pub(crate) mod apis;
|
|
||||||
// Export Context
|
// Export Context
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use rquickjs::function::Args;
|
use rquickjs::function::Args;
|
||||||
use rquickjs::{Array, CatchResultExt, Ctx, Filter, Function, IntoJs, Object, Promise};
|
use rquickjs::{Array, CatchResultExt, Ctx, Filter, FromJs, Function, IntoJs, Object, Promise};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@ -120,7 +120,7 @@ where
|
|||||||
.map_err(|e| anyhow!("{e}"))?;
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
let js_fn: Function<'b> = core_val.get(name).map_err(|e| anyhow!("{e}"))?;
|
let js_fn: Function<'b> = core_val.get(name).map_err(|e| anyhow!("{e}"))?;
|
||||||
let mut args_js = Args::new(ctx.clone(), args.len() as usize);
|
let mut args_js = Args::new(ctx.clone(), args.len() as usize);
|
||||||
for arg in args.iter() {
|
for (i, arg) in args.iter().enumerate() {
|
||||||
let arg_value = serde_json::to_value(arg).map_err(|e| anyhow!("{e}"))?;
|
let arg_value = serde_json::to_value(arg).map_err(|e| anyhow!("{e}"))?;
|
||||||
let arg_js = json_value_to_js(&arg_value, ctx.clone()).map_err(|e| anyhow!("{e}"))?;
|
let arg_js = json_value_to_js(&arg_value, ctx.clone()).map_err(|e| anyhow!("{e}"))?;
|
||||||
args_js.push_arg(arg_js).map_err(|e| anyhow!("{e}"))?;
|
args_js.push_arg(arg_js).map_err(|e| anyhow!("{e}"))?;
|
||||||
@ -161,7 +161,7 @@ where
|
|||||||
.map_err(|e| anyhow!("{e}"))?;
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
let js_fn: Function<'b> = core_val.get(name).map_err(|e| anyhow!("{e}"))?;
|
let js_fn: Function<'b> = core_val.get(name).map_err(|e| anyhow!("{e}"))?;
|
||||||
let mut args_js = Args::new(ctx.clone(), args.len() as usize);
|
let mut args_js = Args::new(ctx.clone(), args.len() as usize);
|
||||||
for arg in args.iter().enumerate() {
|
for (i, arg) in args.iter().enumerate() {
|
||||||
let arg_value = serde_json::to_value(arg).map_err(|e| anyhow!("{e}"))?;
|
let arg_value = serde_json::to_value(arg).map_err(|e| anyhow!("{e}"))?;
|
||||||
let arg_js = json_value_to_js(&arg_value, ctx.clone()).map_err(|e| anyhow!("{e}"))?;
|
let arg_js = json_value_to_js(&arg_value, ctx.clone()).map_err(|e| anyhow!("{e}"))?;
|
||||||
args_js.push_arg(arg_js).map_err(|e| anyhow!("{e}"))?;
|
args_js.push_arg(arg_js).map_err(|e| anyhow!("{e}"))?;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
mod api;
|
mod api;
|
||||||
mod frb_generated;
|
|
||||||
mod internal;
|
mod internal;
|
||||||
|
|
||||||
use rquickjs::function::{Async, Func};
|
use rquickjs::function::{Async, Func};
|
||||||
@ -68,7 +67,9 @@ function sleep(ms) {
|
|||||||
class Core {
|
class Core {
|
||||||
async checkUpdate() {
|
async checkUpdate() {
|
||||||
console.log('Core checkUpdate');
|
console.log('Core checkUpdate');
|
||||||
await sleep(1000);
|
const response = await fetch('https://api.github.com/repos/KRTirtho/spotube/releases/latest');
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data);
|
||||||
console.log('No update available');
|
console.log('No update available');
|
||||||
}
|
}
|
||||||
support() {
|
support() {
|
||||||
@ -76,12 +77,9 @@ class Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Auth {}
|
|
||||||
|
|
||||||
class TestingPlugin {
|
class TestingPlugin {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.core = new Core();
|
this.core = new Core();
|
||||||
this.auth = new Auth();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
";
|
";
|
||||||
@ -100,10 +98,10 @@ async fn plugin() -> anyhow::Result<()> {
|
|||||||
repository: None,
|
repository: None,
|
||||||
version: "0.1.0".to_string(),
|
version: "0.1.0".to_string(),
|
||||||
};
|
};
|
||||||
let sender = plugin.create_context(PLUGIN_JS.to_string(), config.clone(), "".to_string(), "".to_string())?;
|
let sender = SpotubePlugin::new_context(PLUGIN_JS.to_string(), config.clone())?;
|
||||||
let (r1, r2) = tokio::join!(
|
let (r1, r2) = tokio::join!(
|
||||||
plugin.core.check_update(&sender, config.clone()),
|
plugin.core.check_update(sender.clone(), config.clone()),
|
||||||
plugin.core.check_update(&sender, config.clone())
|
plugin.core.check_update(sender.clone(), config.clone())
|
||||||
);
|
);
|
||||||
r1?;
|
r1?;
|
||||||
r2?;
|
r2?;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user