mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat: add webviiew, totp and setInterval apis for plugins
This commit is contained in:
parent
abe04b28b2
commit
f4306ad1c3
@ -231,5 +231,9 @@ class AppRouter extends RootStackRouter {
|
|||||||
page: LastFMLoginRoute.page,
|
page: LastFMLoginRoute.page,
|
||||||
// parentNavigatorKey: rootNavigatorKey,
|
// parentNavigatorKey: rootNavigatorKey,
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "/webview",
|
||||||
|
page: WebviewRoute.page,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
56
lib/pages/webview/webview.dart
Normal file
56
lib/pages/webview/webview.dart
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
import 'package:spotube/components/button/back_button.dart';
|
||||||
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
|
import 'package:spotube/services/metadata/apis/webview.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class WebviewPage extends StatelessWidget {
|
||||||
|
final WebviewInitialSettings? initialSettings;
|
||||||
|
final String? url;
|
||||||
|
final void Function(InAppWebViewController controller, WebUri? url)?
|
||||||
|
onLoadStop;
|
||||||
|
|
||||||
|
const WebviewPage({
|
||||||
|
super.key,
|
||||||
|
this.initialSettings,
|
||||||
|
this.url,
|
||||||
|
this.onLoadStop,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: Scaffold(
|
||||||
|
headers: const [
|
||||||
|
TitleBar(
|
||||||
|
leading: [BackButton(color: Colors.white)],
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
floatingHeader: true,
|
||||||
|
child: InAppWebView(
|
||||||
|
initialSettings: InAppWebViewSettings(
|
||||||
|
userAgent: initialSettings?.userAgent ??
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 safari/537.36",
|
||||||
|
incognito: initialSettings?.incognito ?? false,
|
||||||
|
clearCache: initialSettings?.clearCache ?? false,
|
||||||
|
clearSessionCache: initialSettings?.clearSessionCache ?? false,
|
||||||
|
),
|
||||||
|
initialUrlRequest: URLRequest(
|
||||||
|
url: WebUri("https://accounts.spotify.com/"),
|
||||||
|
),
|
||||||
|
onPermissionRequest: (controller, permissionRequest) async {
|
||||||
|
return PermissionResponse(
|
||||||
|
resources: permissionRequest.resources,
|
||||||
|
action: PermissionResponseAction.GRANT,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onLoadStop: onLoadStop,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
73
lib/services/metadata/apis/set_interval.dart
Normal file
73
lib/services/metadata/apis/set_interval.dart
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter_js/flutter_js.dart';
|
||||||
|
|
||||||
|
class PluginSetIntervalApi {
|
||||||
|
final JavascriptRuntime runtime;
|
||||||
|
|
||||||
|
final Map<String, Timer> _timers = {};
|
||||||
|
|
||||||
|
PluginSetIntervalApi(this.runtime) {
|
||||||
|
runtime.evaluate(
|
||||||
|
"""
|
||||||
|
var __NATIVE_FLUTTER_JS__setIntervalCount = -1;
|
||||||
|
var __NATIVE_FLUTTER_JS__setIntervalCallbacks = {};
|
||||||
|
function setInterval(fnInterval, interval) {
|
||||||
|
try {
|
||||||
|
__NATIVE_FLUTTER_JS__setIntervalCount += 1;
|
||||||
|
var intervalIndex = '' + __NATIVE_FLUTTER_JS__setIntervalCount;
|
||||||
|
__NATIVE_FLUTTER_JS__setIntervalCallbacks[intervalIndex] = fnInterval;
|
||||||
|
;
|
||||||
|
sendMessage('PluginSetIntervalApi.setInterval', JSON.stringify({ intervalIndex, interval}));
|
||||||
|
return intervalIndex;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('ERROR HERE',e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function clearInterval(intervalIndex) {
|
||||||
|
try {
|
||||||
|
delete __NATIVE_FLUTTER_JS__setIntervalCallbacks[intervalIndex];
|
||||||
|
sendMessage('PluginSetIntervalApi.clearInterval', JSON.stringify({ intervalIndex}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('ERROR HERE',e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
1
|
||||||
|
""",
|
||||||
|
);
|
||||||
|
|
||||||
|
runtime.onMessage('PluginSetIntervalApi.setInterval', (dynamic args) {
|
||||||
|
try {
|
||||||
|
int duration = args['interval'] ?? 0;
|
||||||
|
String idx = args['intervalIndex'];
|
||||||
|
|
||||||
|
_timers[idx] =
|
||||||
|
Timer.periodic(Duration(milliseconds: duration), (timer) {
|
||||||
|
runtime.evaluate("""
|
||||||
|
__NATIVE_FLUTTER_JS__setIntervalCallbacks[$idx].call();
|
||||||
|
delete __NATIVE_FLUTTER_JS__setIntervalCallbacks[$idx];
|
||||||
|
""");
|
||||||
|
});
|
||||||
|
} on Exception catch (e) {
|
||||||
|
print('Exception no setInterval: $e');
|
||||||
|
} on Error catch (e) {
|
||||||
|
print('Erro no setInterval: $e');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
runtime.onMessage('PluginSetIntervalApi.clearInterval', (dynamic args) {
|
||||||
|
try {
|
||||||
|
String idx = args['intervalIndex'];
|
||||||
|
if (_timers.containsKey(idx)) {
|
||||||
|
_timers[idx]?.cancel();
|
||||||
|
_timers.remove(idx);
|
||||||
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
print('Exception no clearInterval: $e');
|
||||||
|
} on Error catch (e) {
|
||||||
|
print('Erro no clearInterval: $e');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
40
lib/services/metadata/apis/totp.dart
Normal file
40
lib/services/metadata/apis/totp.dart
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import 'package:flutter_js/javascript_runtime.dart';
|
||||||
|
import 'package:otp_util/otp_util.dart';
|
||||||
|
// ignore: implementation_imports
|
||||||
|
import 'package:otp_util/src/utils/generic_util.dart';
|
||||||
|
|
||||||
|
class PluginTotpGenerator {
|
||||||
|
final JavascriptRuntime runtime;
|
||||||
|
|
||||||
|
PluginTotpGenerator(this.runtime) {
|
||||||
|
runtime.onMessage("TotpGenerator.generate", (args) {
|
||||||
|
final opts = args[0];
|
||||||
|
if (opts is! Map) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final totp = TOTP(
|
||||||
|
secret: opts["secret"] as String,
|
||||||
|
algorithm: OTPAlgorithm.values.firstWhere(
|
||||||
|
(e) => e.name == opts["algorithm"],
|
||||||
|
orElse: () => OTPAlgorithm.SHA1,
|
||||||
|
),
|
||||||
|
digits: opts["digits"] as int? ?? 6,
|
||||||
|
interval: opts["interval"] as int? ?? 30,
|
||||||
|
);
|
||||||
|
|
||||||
|
final otp = totp.generateOTP(
|
||||||
|
input: Util.timeFormat(
|
||||||
|
time: DateTime.fromMillisecondsSinceEpoch(opts["period"]),
|
||||||
|
interval: 30,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
runtime.evaluate(
|
||||||
|
"""
|
||||||
|
eventEmitter.emit('Totp.generate', '$otp');
|
||||||
|
""",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
160
lib/services/metadata/apis/webview.dart
Normal file
160
lib/services/metadata/apis/webview.dart
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:desktop_webview_window/desktop_webview_window.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
|
import 'package:flutter_js/flutter_js.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart' hide join;
|
||||||
|
import 'package:spotube/collections/routes.dart';
|
||||||
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
|
import 'package:spotube/pages/mobile_login/no_webview_runtime_dialog.dart';
|
||||||
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
|
class WebviewInitialSettings {
|
||||||
|
final String? userAgent;
|
||||||
|
final bool? incognito;
|
||||||
|
final bool? clearCache;
|
||||||
|
final bool? clearSessionCache;
|
||||||
|
|
||||||
|
WebviewInitialSettings({
|
||||||
|
this.userAgent,
|
||||||
|
this.incognito,
|
||||||
|
this.clearCache,
|
||||||
|
this.clearSessionCache,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory WebviewInitialSettings.fromJson(Map<String, dynamic> json) {
|
||||||
|
return WebviewInitialSettings(
|
||||||
|
userAgent: json["userAgent"],
|
||||||
|
incognito: json["incognito"],
|
||||||
|
clearCache: json["clearCache"],
|
||||||
|
clearSessionCache: json["clearSessionCache"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PluginWebViewApi {
|
||||||
|
JavascriptRuntime runtime;
|
||||||
|
|
||||||
|
PluginWebViewApi({
|
||||||
|
required this.runtime,
|
||||||
|
}) {
|
||||||
|
runtime.onMessage("WebView.show", (args) {
|
||||||
|
if (args[0] is! Map) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showWebView(
|
||||||
|
url: args[0]["url"] as String,
|
||||||
|
initialSettings: WebviewInitialSettings.fromJson(
|
||||||
|
args[0]["initialSettings"],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future showWebView({
|
||||||
|
required String url,
|
||||||
|
WebviewInitialSettings? initialSettings,
|
||||||
|
}) async {
|
||||||
|
if (rootNavigatorKey.currentContext == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final context = rootNavigatorKey.currentContext!;
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
if (kIsMobile || kIsMacOS) {
|
||||||
|
context.pushRoute(WebviewRoute(
|
||||||
|
initialSettings: initialSettings,
|
||||||
|
url: url,
|
||||||
|
onLoadStop: (controller, uri) async {
|
||||||
|
if (uri == null) return;
|
||||||
|
final cookies = await CookieManager().getAllCookies();
|
||||||
|
|
||||||
|
final jsonCookies = cookies.map((e) {
|
||||||
|
return {
|
||||||
|
"name": e.name,
|
||||||
|
"value": e.value,
|
||||||
|
"domain": e.domain,
|
||||||
|
"path": e.path,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
runtime.onMessage("WebView.close", (args) {
|
||||||
|
context.back();
|
||||||
|
});
|
||||||
|
|
||||||
|
runtime.evaluate(
|
||||||
|
"""
|
||||||
|
eventEmitter.emit('WebView.onLoadFinish', {url: '${uri.toString()}', cookies: ${jsonEncode(jsonCookies)}});
|
||||||
|
""",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final applicationSupportDir = await getApplicationSupportDirectory();
|
||||||
|
final userDataFolder = Directory(
|
||||||
|
join(applicationSupportDir.path, "webview_window_Webview2"),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!await userDataFolder.exists()) {
|
||||||
|
await userDataFolder.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
final webview = await WebviewWindow.create(
|
||||||
|
configuration: CreateConfiguration(
|
||||||
|
title: "Webview",
|
||||||
|
titleBarTopPadding: kIsMacOS ? 20 : 0,
|
||||||
|
windowHeight: 720,
|
||||||
|
windowWidth: 1280,
|
||||||
|
userDataFolderWindows: userDataFolder.path,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
runtime.onMessage("WebView.close", (args) {
|
||||||
|
webview.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
webview
|
||||||
|
..setBrightness(theme.colorScheme.brightness)
|
||||||
|
..launch(url)
|
||||||
|
..setOnUrlRequestCallback((url) {
|
||||||
|
() async {
|
||||||
|
final cookies = await webview.getAllCookies();
|
||||||
|
final jsonCookies = cookies.map((e) {
|
||||||
|
return {
|
||||||
|
"name": e.name,
|
||||||
|
"value": e.value,
|
||||||
|
"domain": e.domain,
|
||||||
|
"path": e.path,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
runtime.evaluate(
|
||||||
|
"""
|
||||||
|
eventEmitter.emit('WebView.onLoadFinish', {url: '$url', cookies: ${jsonEncode(jsonCookies)}});
|
||||||
|
""",
|
||||||
|
);
|
||||||
|
}();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
} on PlatformException catch (_) {
|
||||||
|
if (!await WebviewWindow.isWebviewAvailable()) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return const NoWebviewRuntimeDialog();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,16 +8,45 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:spotube/services/metadata/apis/localstorage.dart';
|
import 'package:spotube/services/metadata/apis/localstorage.dart';
|
||||||
|
import 'package:spotube/services/metadata/apis/set_interval.dart';
|
||||||
|
import 'package:spotube/services/metadata/apis/totp.dart';
|
||||||
|
import 'package:spotube/services/metadata/apis/webview.dart';
|
||||||
|
|
||||||
const defaultMetadataLimit = "20";
|
const defaultMetadataLimit = "20";
|
||||||
|
|
||||||
|
class MetadataSignatureFlags {
|
||||||
|
final bool requiresAuth;
|
||||||
|
|
||||||
|
const MetadataSignatureFlags({
|
||||||
|
this.requiresAuth = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MetadataSignatureFlags.fromJson(Map<String, dynamic> json) {
|
||||||
|
return MetadataSignatureFlags(
|
||||||
|
requiresAuth: json["requiresAuth"] ?? false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Signature for metadata and related methods that will return Spotube native
|
/// Signature for metadata and related methods that will return Spotube native
|
||||||
/// objects e.g. SpotubeTrack, SpotubePlaylist, etc.
|
/// objects e.g. SpotubeTrack, SpotubePlaylist, etc.
|
||||||
class MetadataApiSignature {
|
class MetadataApiSignature {
|
||||||
final JavascriptRuntime runtime;
|
final JavascriptRuntime runtime;
|
||||||
final PluginLocalStorageApi localStorageApi;
|
final PluginLocalStorageApi localStorageApi;
|
||||||
|
final PluginWebViewApi webViewApi;
|
||||||
|
final PluginTotpGenerator totpGenerator;
|
||||||
|
final PluginSetIntervalApi setIntervalApi;
|
||||||
|
late MetadataSignatureFlags _signatureFlags;
|
||||||
|
|
||||||
MetadataApiSignature._(this.runtime, this.localStorageApi);
|
MetadataSignatureFlags get signatureFlags => _signatureFlags;
|
||||||
|
|
||||||
|
MetadataApiSignature._(
|
||||||
|
this.runtime,
|
||||||
|
this.localStorageApi,
|
||||||
|
this.webViewApi,
|
||||||
|
this.totpGenerator,
|
||||||
|
this.setIntervalApi,
|
||||||
|
);
|
||||||
|
|
||||||
static Future<MetadataApiSignature> init(
|
static Future<MetadataApiSignature> init(
|
||||||
String libraryCode, PluginConfiguration config) async {
|
String libraryCode, PluginConfiguration config) async {
|
||||||
@ -52,10 +81,21 @@ class MetadataApiSignature {
|
|||||||
pluginName: config.slug,
|
pluginName: config.slug,
|
||||||
);
|
);
|
||||||
|
|
||||||
return MetadataApiSignature._(
|
final webViewApi = PluginWebViewApi(runtime: runtime);
|
||||||
|
final totpGenerator = PluginTotpGenerator(runtime);
|
||||||
|
final setIntervalApi = PluginSetIntervalApi(runtime);
|
||||||
|
|
||||||
|
final metadataApi = MetadataApiSignature._(
|
||||||
runtime,
|
runtime,
|
||||||
localStorageApi,
|
localStorageApi,
|
||||||
|
webViewApi,
|
||||||
|
totpGenerator,
|
||||||
|
setIntervalApi,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
metadataApi._signatureFlags = await metadataApi._getSignatureFlags();
|
||||||
|
|
||||||
|
return metadataApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -104,6 +144,18 @@ class MetadataApiSignature {
|
|||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<MetadataSignatureFlags> _getSignatureFlags() async {
|
||||||
|
final res = await invoke("metadataApi.getSignatureFlags");
|
||||||
|
|
||||||
|
return MetadataSignatureFlags.fromJson(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Authentication ------
|
||||||
|
|
||||||
|
Future<void> authenticate() async {
|
||||||
|
await invoke("metadataApi.authenticate");
|
||||||
|
}
|
||||||
|
|
||||||
// ----- Track ------
|
// ----- Track ------
|
||||||
Future<SpotubeTrackObject> getTrack(String id) async {
|
Future<SpotubeTrackObject> getTrack(String id) async {
|
||||||
final result = await invoke("metadataApi.getTrack", [id]);
|
final result = await invoke("metadataApi.getTrack", [id]);
|
||||||
|
Loading…
Reference in New Issue
Block a user