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,
|
||||
// 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/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]);
|
||||
|
Loading…
Reference in New Issue
Block a user