Skip to content
Draft
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
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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
38 changes: 37 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,36 @@ interface AcodeFile {
declare global {
var Executor: Executor | undefined;
}

interface WebViewOptions {
title?: string;
mode?: "fullscreen" | "window" | "panel" | "hidden";
width?: number;
height?: number;
x?: number;
y?: number;
allowNavigation?: boolean;
allowDownloads?: boolean;
visible?: boolean;
}

interface AcodeWebView {
readonly id: string;
readonly options: WebViewOptions;
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;
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(): Promise<void>;
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
159 changes: 159 additions & 0 deletions src/lib/webview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import nativeBridge from "../plugins/webview/www/webview";

Check failure on line 1 in src/lib/webview.js

View workflow job for this annotation

GitHub Actions / Linting and formatting

format

File content differs from formatting output

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

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

if (event) {
const callbacks = eventCallbacks.get(id);
if (callbacks) {
callbacks.forEach((entry) => {
if (entry.event === event) {
try {
entry.callback(event, data);
} catch (e) {
console.error("WebView event callback error:", e);
}
}
});
}
return;
}

if (message !== undefined) {
const instance = instances.get(id);
if (instance && instance._messageCallbacks) {
let parsed = message;
try {
parsed = JSON.parse(message);
} catch (_) {}
instance._messageCallbacks.forEach((cb) => {
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);
eventCallbacks.set(id, this._eventCallbacks);
}

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);
eventCallbacks.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 id = await nativeBridge.create({
title: options.title || "",
mode: options.mode || "hidden",
width: options.width || 0,
height: options.height || 0,
x: options.x || 0,
y: options.y || 0,
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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.foxdebug.webview;

import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.widget.FrameLayout;

public class WebViewActivity extends Activity {

private static WebViewPlugin plugin;

private WebView webView;
private String webviewId;

public static void setPlugin(WebViewPlugin p) {
plugin = p;
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

Intent intent = getIntent();
webviewId = intent.getStringExtra("webviewId");

WebViewInstance instance = plugin != null ? plugin.getInstance(webviewId) : null;
if (instance == null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Fullscreen still closes immediately. WebViewActivity reads its private static plugin field, but the fullscreen launch path never calls setPlugin(). The field remains null, so creating a fullscreen WebView makes instance null and immediately finishes the activity without displaying any content. Initialize this reference before launching the activity or retrieve the initialized WebViewPlugin singleton.

finish();
return;
}

instance.createWebView(this);
webView = instance.getWebView();

FrameLayout container = new FrameLayout(this);
container.addView(webView, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
));
setContentView(container);

if (Build.VERSION.SDK_INT >= 30) {
getWindow().setDecorFitsSystemWindows(false);
}
}

@Override
public void onBackPressed() {
if (webView != null && webView.canGoBack()) {
webView.goBack();
} else {
finish();
}
}

@Override
protected void onDestroy() {
super.onDestroy();
if (plugin != null) {
plugin.sendEventToCordova(webviewId, "closed", null);
plugin.removeInstance(webviewId);
}
}
}
Loading
Loading