async function installPlugin(
	id,
	name,
	purchaseToken,
	isDependency,
) {
	if (!isDependency) {
		loaderDialog = loader.create(name || "Plugin", strings.installing, {
			timeout: 6000,
		});
		depsLoaders = [];
	}

	let pluginDir;
	let pluginUrl;
	let state;

	try {
		if (!(await fsOperation(PLUGIN_DIR).exists())) {
			await fsOperation(DATA_STORAGE).createDirectory("plugins");
		}
	} catch (error) {
		window.log("error", error);
	}

	if (!/^(https?|file|content):/.test(id)) {
		pluginUrl = Url.join(
			constants.API_BASE,
			"plugin/download/",
			`${id}?device=${device.uuid}`,
		);
		if (purchaseToken) pluginUrl += `&token=${purchaseToken}`;
		pluginUrl += `&package=${BuildInfo.packageName}`;
		pluginUrl += `&version=${device.version}`;

		pluginDir = Url.join(PLUGIN_DIR, id);
	} else {
		pluginUrl = id;
	}

	try {
		if (!isDependency) loaderDialog.show();

		let plugin;
		if (
			pluginUrl.includes(constants.API_BASE) ||
			pluginUrl.startsWith("file:") ||
			pluginUrl.startsWith("content:")
		) {
			// Use fsOperation for Acode registry URL
			plugin = await fsOperation(pluginUrl).readFile(
				undefined,
				(loaded, total) => {
					loaderDialog.setMessage(
						`${strings.loading} ${((loaded / total) * 100).toFixed(2)}%`,
					);
				},
			);
		} else {
			// cordova http plugin for others
			plugin = await new Promise((resolve, reject) => {
				cordova.plugin.http.sendRequest(
					pluginUrl,
					{
						method: "GET",
						responseType: "arraybuffer",
					},
					(response) => {
						resolve(response.data);
						loaderDialog.setMessage(`${strings.loading} 100%`);
					},
					(error) => {
						reject(error);
					},
				);
			});
		}

		if (plugin) {
			const zip = new JSZip();
			await zip.loadAsync(plugin);

			if (!zip.files["plugin.json"]) {
				throw new Error(strings["invalid plugin"]);
			}

			/** @type {{ dependencies: string[] }} */
			const pluginJson = JSON.parse(
				await zip.files["plugin.json"].async("text"),
			);

			/** patch main in manifest */
			if (!zip.files[pluginJson.main]) {
				pluginJson.main = "main.js";
			}

			/** patch icon in manifest */
			if (!zip.files[pluginJson.icon]) {
				pluginJson.icon = "icon.png";
			}

			/** patch readme in manifest */
			if (!zip.files[pluginJson.readme]) {
				pluginJson.readme = "readme.md";
			}

			if (!zip.files[pluginJson.main]) {
				throw new Error(strings["invalid plugin"]);
			}

			if (!isDependency && pluginJson.dependencies) {
				const manifests = await resolveDepsManifest(pluginJson.dependencies);

				let titleText;
				if (manifests.length > 1) {
					titleText = "Acode wants to install the following dependencies:";
				} else {
					titleText = "Acode wants to install the following dependency:";
				}

				const shouldInstall = await confirm(
					"Installer Notice",
					titleText +
						"<br /><br />" +
						manifests.map((value) => value.name).join(", "),
					true,
				);

				if (shouldInstall) {
					for (const manifest of manifests) {
						const hasError = await resolveDep(manifest);
						if (hasError) throw new Error(strings.failed);
					}
				} else {
					return;
				}
			}

			if (!pluginDir) {
				pluginJson.source = pluginUrl;
				id = pluginJson.id;
				pluginDir = Url.join(PLUGIN_DIR, id);
			}

			state = await InstallState.new(id);

			if (!(await fsOperation(pluginDir).exists())) {
				await fsOperation(PLUGIN_DIR).createDirectory(id);
			}

			const promises = Object.keys(zip.files).map(async (file) => {
				try {
					let correctFile = file;
					if (/\\/.test(correctFile)) {
						correctFile = correctFile.replace(/\\/g, "/");
					}

					const fileUrl = Url.join(pluginDir, correctFile);

					if (!state.exists(correctFile)) {
						await createFileRecursive(pluginDir, correctFile);
					}

					// Skip directories
					if (correctFile.endsWith("/")) return;

					let data = await zip.files[file].async("ArrayBuffer");

					if (file === "plugin.json") {
						data = JSON.stringify(pluginJson);
					}

					if (!(await state.isUpdated(correctFile, data))) return;
					await fsOperation(fileUrl).writeFile(data);
					return;
				} catch (error) {
					console.error(`Error processing file ${file}:`, error);
				}
			});

			// Wait for all files to be processed
			await Promise.allSettled(promises);

			if (isDependency) {
				depsLoaders.push(async () => {
					await loadPlugin(id, true);
				});
			} else {
				for (const loader of depsLoaders) {
					await loader();
				}
				await loadPlugin(id, true);
			}

			await state.save();
			deleteRedundantFiles(pluginDir, state);
		}
	} catch (err) {
		try {
			// Clear the install state if installation fails
			if (state) await state.clear();

			// Delete the plugin directory if it was created
			if (pluginDir && (await fsOperation(pluginDir).exists())) {
				await fsOperation(pluginDir).delete();
			}
		} catch (cleanupError) {
			console.error("Cleanup failed:", cleanupError);
		}
		throw err;
	} finally {
		if (!isDependency) {
			loaderDialog.destroy();
		}
	}
}
Installs a plugin.
@param {string} id
@param {string} name
@param {string} purchaseToken
@param {boolean} isDependency