feat: add webviiew, totp and setInterval apis for plugins

This commit is contained in:
Kingkor Roy Tirtho 2025-05-07 23:39:44 +06:00
parent abe04b28b2
commit f4306ad1c3
7 changed files with 618 additions and 165 deletions

View File

@ -231,5 +231,9 @@ class AppRouter extends RootStackRouter {
page: LastFMLoginRoute.page,
// parentNavigatorKey: rootNavigatorKey,
),
AutoRoute(
path: "/webview",
page: WebviewRoute.page,
),
];
}

File diff suppressed because it is too large Load Diff

View 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,
),
),
);
}
}

View 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');
}
});
}
}

View 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');
""",
);
});
}
}

View 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();
},
);
});
}
}
}
}

View File

@ -8,16 +8,45 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/services/logger/logger.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";
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
/// objects e.g. SpotubeTrack, SpotubePlaylist, etc.
class MetadataApiSignature {
final JavascriptRuntime runtime;
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(
String libraryCode, PluginConfiguration config) async {
@ -52,10 +81,21 @@ class MetadataApiSignature {
pluginName: config.slug,
);
return MetadataApiSignature._(
final webViewApi = PluginWebViewApi(runtime: runtime);
final totpGenerator = PluginTotpGenerator(runtime);
final setIntervalApi = PluginSetIntervalApi(runtime);
final metadataApi = MetadataApiSignature._(
runtime,
localStorageApi,
webViewApi,
totpGenerator,
setIntervalApi,
);
metadataApi._signatureFlags = await metadataApi._getSignatureFlags();
return metadataApi;
}
void dispose() {
@ -104,6 +144,18 @@ class MetadataApiSignature {
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 ------
Future<SpotubeTrackObject> getTrack(String id) async {
final result = await invoke("metadataApi.getTrack", [id]);