Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,596 changes: 1,273 additions & 2,323 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"com.foxdebug.acode.rk.customtabs": {},
"cordova-plugin-system": {},
"cordova-plugin-crashhandler": {},
"cordova-plugin-advanced-http": {}
"cordova-plugin-advanced-http": {},
"cordova-plugin-acode-webview": {}
},
"platforms": [
"android"
Expand Down Expand Up @@ -84,6 +85,7 @@
"com.foxdebug.acode.rk.plugin.plugincontext": "file:src/plugins/pluginContext",
"cordova-android": "^15.0.0",
"cordova-clipboard": "^1.3.0",
"cordova-plugin-acode-webview": "file:src/plugins/webview",
"cordova-plugin-advanced-http": "file:src/plugins/cordova-plugin-advanced-http",
"cordova-plugin-browser": "file:src/plugins/browser",
"cordova-plugin-buildinfo": "file:src/plugins/cordova-plugin-buildinfo",
Expand Down
67 changes: 66 additions & 1 deletion src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ declare const PLUGIN_DIR: string;
declare const KEYBINDING_FILE: string;
declare const ANDROID_SDK_INT: number;
declare const DOES_SUPPORT_THEME: boolean;
declare const acode: object;
declare const acode: {
webview: AcodeWebViewAPI;
[key: string]: unknown;
};

interface Window {
ASSETS_DIRECTORY: string;
Expand Down Expand Up @@ -107,3 +110,65 @@ interface AcodeFile {
declare global {
var Executor: Executor | undefined;
}

interface WebViewOptions {
/** Title applied to the hosting activity in fullscreen mode. */
title?: string;
/**
* "fullscreen" displays the WebView in its own activity, "hidden" runs it
* headless (never displayed). Defaults to "hidden".
*/
mode?: "fullscreen" | "hidden";
/**
* Allow in-WebView navigation. Defaults to true. Only http(s) targets
* ever load; other schemes are always blocked for isolation.
*/
allowNavigation?: boolean;
/**
* Ask the user with a confirmation dialog before downloading files via
* the system DownloadManager. Defaults to false.
*/
allowDownloads?: boolean;
/**
* Show immediately after creation. Defaults to true. Only meaningful for
* fullscreen mode: when false, the activity launch is deferred until
* show() is called.
*/
visible?: boolean;
}

interface AcodeWebView {
readonly id: string;
readonly options: WebViewOptions;
/** Load an http(s) URL. Other schemes are rejected. */
loadURL(url: string): Promise<void>;
loadHTML(html: string): Promise<void>;
evaluate(js: string): Promise<string>;
onMessage(callback: (message: unknown) => void): void;
offMessage(callback: (message: unknown) => void): void;
/**
* Subscribe to lifecycle events: "pageFinished", "titleChanged" and
* "closed" (fullscreen closed by the user or the system). After "closed"
* the instance is destroyed and cannot be reused.
*/
on(event: string, callback: (event: string, data?: unknown) => void): void;
off(event: string, callback: (event: string, data?: unknown) => void): void;
postMessage(message: unknown): Promise<void>;
/**
* Show the WebView. Only fullscreen instances can be shown; rejects for
* "hidden" mode.
*/
show(): Promise<void>;
/**
* Hide the WebView. Fullscreen instances are moved to the background;
* show() brings the same WebView back with its page state intact.
* No-op for "hidden" mode.
*/
hide(): Promise<void>;
reload(): Promise<void>;
destroy(): Promise<void>;
}

interface AcodeWebViewAPI {
create(options?: WebViewOptions): Promise<AcodeWebView>;
}
2 changes: 2 additions & 0 deletions src/lib/acode.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import helpers from "utils/helpers";
import KeyboardEvent from "utils/keyboardEvent";
import Url from "utils/Url";
import config from "./config";
import webview from "./webview";

class Acode {
#modules = {};
Expand Down Expand Up @@ -396,6 +397,7 @@ class Acode {
this.define("selectionMenu", selectionMenu);
this.define("sidebarApps", sidebarAppsModule);
this.define("terminal", terminalModule);
this.define("webview", webview);
this.define("codemirror", codemirrorModule);
this.define("@codemirror/autocomplete", cmAutocomplete);
this.define("@codemirror/commands", cmCommands);
Expand Down
166 changes: 166 additions & 0 deletions src/lib/webview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import nativeBridge from "../plugins/webview/www/webview";

let initialized = false;
const instances = new Map();

function ensureInit() {
if (!initialized) {
nativeBridge.setMessageCallback((payload) => {
const { id, message, event, data } = payload;
const instance = instances.get(id);
if (!instance) return;

if (event) {
// Iterate a copy so callbacks can unsubscribe during dispatch.
for (const entry of instance._eventCallbacks.slice()) {
if (entry.event === event) {
try {
entry.callback(event, data);
} catch (e) {
console.error("WebView event callback error:", e);
}
}
}

// A fullscreen WebView closed by the user (or by hide()) is
// gone natively; mark it destroyed so later calls fail fast
// and its listeners can be garbage collected.
if (event === "closed") {
instance._destroyed = true;
instances.delete(id);
instance._messageCallbacks = [];
instance._eventCallbacks = [];
}
return;
}

if (message !== undefined) {
let parsed = message;
try {
parsed = JSON.parse(message);
} catch (_) {}
for (const cb of instance._messageCallbacks.slice()) {
try {
cb(parsed);
} catch (e) {
console.error("WebView message callback error:", e);
}
}
}
});
initialized = true;
}
}

class WebView {
constructor(id, options = {}) {
this.id = id;
this.options = options;
this._messageCallbacks = [];
this._eventCallbacks = [];
this._destroyed = false;

instances.set(id, this);
}

async loadURL(url) {
this._checkDestroyed();
await nativeBridge.loadURL(this.id, url);
}

async loadHTML(html) {
this._checkDestroyed();
await nativeBridge.loadHTML(this.id, html);
}

async evaluate(js) {
this._checkDestroyed();
return await nativeBridge.evaluate(this.id, js);
}

onMessage(callback) {
this._checkDestroyed();
if (typeof callback === "function") {
this._messageCallbacks.push(callback);
}
}

offMessage(callback) {
this._messageCallbacks = this._messageCallbacks.filter(
(cb) => cb !== callback,
);
}

on(event, callback) {
this._checkDestroyed();
if (typeof callback === "function") {
this._eventCallbacks.push({ event, callback });
}
}

off(event, callback) {
this._eventCallbacks = this._eventCallbacks.filter(
(entry) => !(entry.event === event && entry.callback === callback),
);
}

async postMessage(message) {
this._checkDestroyed();
await nativeBridge.postMessage(this.id, message);
}

async show() {
this._checkDestroyed();
await nativeBridge.show(this.id);
}

async hide() {
this._checkDestroyed();
await nativeBridge.hide(this.id);
}

async reload() {
this._checkDestroyed();
await nativeBridge.reload(this.id);
}

async destroy() {
this._checkDestroyed();
this._destroyed = true;
await nativeBridge.destroy(this.id);
instances.delete(this.id);
this._messageCallbacks = [];
this._eventCallbacks = [];
}

_checkDestroyed() {
if (this._destroyed) {
throw new Error("WebView has been destroyed");
}
}
}

const webviewAPI = {
async create(options = {}) {
ensureInit();

const mode = options.mode || "hidden";
if (mode !== "fullscreen" && mode !== "hidden") {
throw new Error(
`Unsupported WebView mode: "${mode}". Use "fullscreen" or "hidden".`,
);
}

const id = await nativeBridge.create({
title: options.title || "",
mode,
allowNavigation: options.allowNavigation !== false,
allowDownloads: options.allowDownloads === true,
visible: options.visible !== false,
});

return new WebView(id, options);
},
};

export default webviewAPI;
11 changes: 11 additions & 0 deletions src/plugins/webview/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "cordova-plugin-acode-webview",
"version": "1.0.0",
"description": "Acode WebView Plugin API",
"main": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "Apache-2.0"
}
26 changes: 26 additions & 0 deletions src/plugins/webview/plugin.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
xmlns:android="http://schemas.android.com/apk/res/android" id="cordova-plugin-acode-webview" version="1.0.0">
<name>cordova-plugin-acode-webview</name>
<description>Acode WebView Plugin API - Create and manage isolated WebView instances</description>
<license>Apache 2.0</license>

<platform name="android">

<config-file target="res/xml/config.xml" parent="/*">
<feature name="AcodeWebView">
<param name="android-package" value="com.foxdebug.webview.WebViewPlugin"/>
</feature>
</config-file>

<config-file parent="./application" target="AndroidManifest.xml">
<activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode" android:name="com.foxdebug.webview.WebViewActivity" android:theme="@style/Theme.App.Activity" android:windowSoftInputMode="adjustResize" android:resizeableActivity="true">
</activity>
</config-file>

<source-file src="src/android/com/foxdebug/webview/WebViewPlugin.java" target-dir="src/com/foxdebug/webview"/>
<source-file src="src/android/com/foxdebug/webview/WebViewInstance.java" target-dir="src/com/foxdebug/webview"/>
<source-file src="src/android/com/foxdebug/webview/WebViewActivity.java" target-dir="src/com/foxdebug/webview"/>

</platform>
</plugin>
Loading