diff --git a/.vscode/settings.json b/.vscode/settings.json index 639360100..2e0caf532 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,13 +1,11 @@ { "editor.formatOnSave": true, - "autoimport.doubleQuotes": false, - "java.configuration.updateBuildConfiguration": "disabled", "prettier.requireConfig": true, - "javascript.format.enable": true, + "js/ts.format.enabled": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "prettier.tabWidth": 2, "prettier.useTabs": false, - "javascript.format.semicolons": "insert", + "js/ts.format.semicolons": "insert", "[scss]": { "editor.defaultFormatter": "vscode.css-language-features" }, @@ -27,6 +25,13 @@ "attr_quotes": "single" } }, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "editor.codeActionsOnSave": { + "quickFix.biome": "explicit", + "source.organizeImports.biome": "explicit" + }, "cSpell.words": [ "abap", "Acode", @@ -126,6 +131,7 @@ "flac", "Flix", "floobits", + "FOXBIZ", "Foxdebug", "freemarker", "gamemaker", @@ -370,10 +376,9 @@ "wtest", "wxml", "wxss", + "xhrs", + "XMLHTTP", "xquery", "Zeek" - ], - "[javascript]": { - "editor.defaultFormatter": "biomejs.biome" - } + ] } diff --git a/biome.json b/biome.json index 1f48da7bc..49e104a29 100644 --- a/biome.json +++ b/biome.json @@ -39,18 +39,15 @@ }, "files": { "includes": [ - "**/src/**/*", - "**/utils/**/*.js", - "!**/www/build/**/*", - "**/www/res/**/*.css", - "**/src/plugins/terminal/**", - "!**/ace-builds", - "!**/src/plugins/**/*", - "!**/plugins/**/*", - "!**/hooks/**/*", - "!**/fastlane/**/*", - "!**/res/**/*", - "!**/platforms/**/*" + "src/**/*.js", + "utils/**/*.js", + "!src/lang/**/*.json", + "!src/plugins/**/*.js", + "!www/**/*", + "!plugins/**/*", + "!hooks/**/*", + "!fastlane/**/*", + "!platforms/**/*" ] } } diff --git a/bun.lock b/bun.lock index 17fef2836..2c111fa7d 100644 --- a/bun.lock +++ b/bun.lock @@ -37,7 +37,6 @@ "@codemirror/state": "^6.6.0", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.40.0", - "@deadlyjack/ajax": "^1.2.6", "@emmetio/codemirror6-plugin": "^0.4.0", "@lezer/highlight": "^1.2.3", "@ungap/custom-elements": "^1.3.0", @@ -425,8 +424,6 @@ "@codemirror/view": ["@codemirror/view@6.40.0", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg=="], - "@deadlyjack/ajax": ["@deadlyjack/ajax@1.2.6", "", {}, "sha512-VwZU8YUflO2/V/dl3dluu+3jg8Ghz/W5fwxD5Z21OZXKeV73d+vStKVBe4wi+Av2KbTR35K7Z+5Q3iIpjB41MA=="], - "@discoveryjs/json-ext": ["@discoveryjs/json-ext@0.5.7", "", {}, "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="], "@emmetio/abbreviation": ["@emmetio/abbreviation@2.3.3", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA=="], diff --git a/hooks/post-process.js b/hooks/post-process.js index 21c411934..e7a175ac3 100644 --- a/hooks/post-process.js +++ b/hooks/post-process.js @@ -30,6 +30,18 @@ enableStaticContext(); patchTargetSdkVersion(); enableKeyboardWorkaround(); +function getPackageName() { + const configPath = path.resolve(__dirname, '../config.xml'); + if (!fs.existsSync(configPath)) { + console.warn('[Cordova Hook] ⚠️ config.xml not found at', configPath); + throw new Error(`config.xml is missing at ${configPath}`); + } + const content = fs.readFileSync(configPath, 'utf-8'); + const match = content.match(/id="([^"]+)"/); + const packageName = match ? match[1] : 'com.foxdebug.acode'; + return packageName; +} + function getTmpDir() { const tmpdirEnv = process.env.TMPDIR; @@ -107,11 +119,17 @@ function enableLegacyJni() { const prefix = execSync('npm prefix').toString().trim(); const gradleFile = path.join(prefix, 'platforms/android/app/build.gradle'); - if (!fs.existsSync(gradleFile)) return; + if (!fs.existsSync(gradleFile)){ + console.warn('[Cordova Hook] ⚠️ build.gradle not found'); + return + }; let content = fs.readFileSync(gradleFile, 'utf-8'); // Check for correct block to avoid duplicate insertion - if (content.includes('useLegacyPackaging = true')) return; + if (content.includes('useLegacyPackaging = true')){ + console.log('[Cordova Hook] ✅ Legacy JNI packaging already enabled, skipping'); + return + }; // Inject under android block with correct Groovy syntax content = content.replace(/android\s*{/, match => { @@ -133,12 +151,16 @@ function enableLegacyJni() { function enableStaticContext() { try { const prefix = execSync('npm prefix').toString().trim(); + const packageName = getPackageName(); const mainActivityPath = path.join( prefix, - 'platforms/android/app/src/main/java/com/foxdebug/acode/MainActivity.java' + 'platforms/android/app/src/main/java', + packageName.replace(/\./g, '/'), + 'MainActivity.java' ); if (!fs.existsSync(mainActivityPath)) { + console.warn('[Cordova Hook] ⚠️ MainActivity.java not found at', mainActivityPath); return; } @@ -150,6 +172,7 @@ function enableStaticContext() { content.includes('public static Context getContext()') && content.includes('weakContext = new WeakReference<>(this);') ) { + console.log('[Cordova Hook] ✅ Static context already enabled, skipping'); return; } @@ -181,6 +204,7 @@ function enableStaticContext() { ); fs.writeFileSync(mainActivityPath, content, 'utf-8'); + console.log('[Cordova Hook] ✅ Enabled static context'); } catch (err) { console.error('[Cordova Hook] ❌ Failed to patch MainActivity:', err.message); } @@ -189,12 +213,16 @@ function enableStaticContext() { function enableKeyboardWorkaround() { try{ const prefix = execSync('npm prefix').toString().trim(); + const packageName = getPackageName(); const mainActivityPath = path.join( prefix, - 'platforms/android/app/src/main/java/com/foxdebug/acode/MainActivity.java' + 'platforms/android/app/src/main/java', + packageName.replace(/\./g, '/'), + 'MainActivity.java' ); if (!fs.existsSync(mainActivityPath)) { + console.warn('[Cordova Hook] ⚠️ MainActivity.java not found at', mainActivityPath); return; } @@ -202,6 +230,7 @@ function enableKeyboardWorkaround() { // Skip if already patched if (content.includes('SoftInputAssist')) { + console.log('[Cordova Hook] ✅ Keyboard workaround already enabled, skipping'); return; } diff --git a/jsconfig.json b/jsconfig.json index 89b465f62..b1983ca52 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,9 +1,8 @@ { "exclude": ["**/node_modules", "**/platforms", "**/www", "www/js/ace/**/*"], "compilerOptions": { - "baseUrl": "./src", "paths": { - "*": ["*"] + "*": ["./src/*"] } }, "include": ["src/**/*"], diff --git a/package-lock.json b/package-lock.json index b78b58a49..72268cd75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,6 @@ "@codemirror/state": "^6.6.0", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.40.0", - "@deadlyjack/ajax": "^1.2.6", "@emmetio/codemirror6-plugin": "^0.4.0", "@lezer/highlight": "^1.2.3", "@ungap/custom-elements": "^1.3.0", @@ -2421,12 +2420,6 @@ "w3c-keyname": "^2.2.4" } }, - "node_modules/@deadlyjack/ajax": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@deadlyjack/ajax/-/ajax-1.2.6.tgz", - "integrity": "sha512-VwZU8YUflO2/V/dl3dluu+3jg8Ghz/W5fwxD5Z21OZXKeV73d+vStKVBe4wi+Av2KbTR35K7Z+5Q3iIpjB41MA==", - "license": "MIT" - }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", diff --git a/package.json b/package.json index f2a2a811d..aed6191ec 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,8 @@ "com.foxdebug.acode.rk.exec.terminal": {}, "com.foxdebug.acode.rk.customtabs": {}, "com.foxdebug.acode.rk.plugin.plugincontext": {}, - "com.foxdebug.acode.rk.auth": {}, - "cordova-plugin-system": {} + "cordova-plugin-system": {}, + "com.foxdebug.acode.rk.auth": {} }, "platforms": [ "android" @@ -137,7 +137,6 @@ "@codemirror/state": "^6.6.0", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.40.0", - "@deadlyjack/ajax": "^1.2.6", "@emmetio/codemirror6-plugin": "^0.4.0", "@lezer/highlight": "^1.2.3", "@ungap/custom-elements": "^1.3.0", diff --git a/src/cm/lsp/serverLauncher.ts b/src/cm/lsp/serverLauncher.ts index 1350a1ff3..8ed527971 100644 --- a/src/cm/lsp/serverLauncher.ts +++ b/src/cm/lsp/serverLauncher.ts @@ -35,6 +35,8 @@ const STATUS_FAILED: InstallStatus = "failed"; const AXS_BINARY = "$PREFIX/axs"; +let alreadyInformed = false; + function getTerminalRequiredMessage(): string { return ( strings?.terminal_required_message_for_lsp ?? @@ -1081,7 +1083,14 @@ export async function ensureServerRunning( } catch {} if (!isTerminalInstalled) { const message = getTerminalRequiredMessage(); - alert(strings?.error, message); + + if (!alreadyInformed){ + alreadyInformed = true; + alert(strings?.error, message); + }else{ + toast(message); + } + const unavailable: LspError = new Error(message); unavailable.code = "LSP_SERVER_UNAVAILABLE"; throw unavailable; diff --git a/src/components/sidebar/index.js b/src/components/sidebar/index.js index 0b606960f..da5202b19 100644 --- a/src/components/sidebar/index.js +++ b/src/components/sidebar/index.js @@ -3,8 +3,17 @@ import toast from "components/toast"; import Ref from "html-tag-js/ref"; import actionStack from "lib/actionStack"; import auth, { loginEvents } from "lib/auth"; -import constants from "lib/constants"; +import config from "lib/config"; +/** + * @typedef {object} SideBar + * @extends HTMLElement + * @property {function():void} hide + * @property {function():void} toggle + * @property {function():void} onshow + */ + +/**@type {HTMLElement} */ let $sidebar; /**@type {Array<(el:HTMLElement)=>boolean>} */ let preventSlideTests = []; @@ -14,14 +23,6 @@ const events = { hide: [], }; -/** - * @typedef {object} SideBar - * @extends HTMLElement - * @property {function():void} hide - * @property {function():void} toggle - * @property {function():void} onshow - */ - /** * Create a sidebar * @param {HTMLElement} [$container] - the element that will contain the sidebar @@ -31,7 +32,7 @@ const events = { function create($container, $toggler) { let { innerWidth } = window; - const START_THRESHOLD = constants.SIDEBAR_SLIDE_START_THRESHOLD_PX; //Point where to start swipe + const START_THRESHOLD = config.SIDEBAR_SLIDE_START_THRESHOLD_PX; //Point where to start swipe const MIN_WIDTH = 200; //Min width of the side bar const MAX_WIDTH = () => innerWidth * 0.7; //Max width of the side bar const resizeBar = Ref(); @@ -103,12 +104,46 @@ function create($container, $toggler) { async function handleUserIconClick(e) { try { - const isLoggedIn = await auth.isLoggedIn(); - - if (!isLoggedIn) { - auth.openLoginUrl(); + const user = await auth.getLoggedInUser(); + + if (!user) { + CustomTabs.open( + `${config.BASE_URL}/login?redirect=app`, + { showTitle: true }, + () => {}, + () => {}, + ); } else { - toggleUserMenu(); + const menu = userContextMenu.el; + const isActive = menu.classList.toggle("active"); + + if (isActive) { + const menuName = userContextMenu.el.querySelector(".user-menu-name"); + const menuEmail = + userContextMenu.el.querySelector(".user-menu-email"); + + if (menuName) { + menuName.content = ( +
+ {Boolean(user.verified) && ( + + )} + {user.name} + {Boolean(user.acode_pro) && Pro} +
+ ); + } + + if (menuEmail) { + menuEmail.textContent = user.email || ""; + } + + setTimeout(() => { + document.addEventListener("click", handleClickOutside); + }, 10); + } else { + document.removeEventListener("click", handleClickOutside); + } } } catch (error) { console.error("Error checking login status:", error); @@ -116,23 +151,6 @@ function create($container, $toggler) { } } - function toggleUserMenu() { - const menu = userContextMenu.el; - const isActive = menu.classList.toggle("active"); - - if (isActive) { - // Populate user info - updateUserMenuInfo(); - - // Add click outside listener - setTimeout(() => { - document.addEventListener("click", handleClickOutside); - }, 10); - } else { - document.removeEventListener("click", handleClickOutside); - } - } - function handleClickOutside(e) { if ( !userContextMenu.el.contains(e.target) && @@ -144,23 +162,6 @@ function create($container, $toggler) { } } - async function updateUserMenuInfo() { - try { - const userInfo = await auth.getUserInfo(); - if (userInfo) { - const menuName = userContextMenu.el.querySelector(".user-menu-name"); - const menuEmail = userContextMenu.el.querySelector(".user-menu-email"); - menuName.textContent = userInfo.name || "Anonymous"; - if (userInfo.isAdmin) { - menuName.innerHTML += ' Admin'; - } - menuEmail.textContent = userInfo.email || ""; - } - } catch (error) { - console.error("Error fetching user info:", error); - } - } - async function handleLogout() { try { const success = await auth.logout(); @@ -178,8 +179,6 @@ function create($container, $toggler) { } async function updateSidebarAvatar() { - const avatarUrl = await auth.getAvatar(); - // Remove existing icon or avatar const existingIcon = userAvatar.el.querySelector(".icon"); const existingAvatar = userAvatar.el.querySelector(".avatar"); @@ -190,20 +189,59 @@ function create($container, $toggler) { existingAvatar.remove(); } - if (avatarUrl?.startsWith("data:") || avatarUrl?.startsWith("http")) { - // Create and add avatar image + const user = await auth.getLoggedInUser(); + + if (user) { + const avatarUrl = user.github + ? `https://avatars.githubusercontent.com/${user.github}` + : generateInitialsAvatar(user.name); const avatarImg = document.createElement("img"); avatarImg.className = "avatar"; avatarImg.src = avatarUrl; userAvatar.append(avatarImg); } else { - // Fallback to default icon const defaultIcon = document.createElement("span"); defaultIcon.className = "icon account_circle"; userAvatar.append(defaultIcon); } } + function generateInitialsAvatar(name) { + const nameParts = name.split(" "); + const initials = + nameParts.length >= 2 + ? `${nameParts[0][0]}${nameParts[1][0]}`.toUpperCase() + : nameParts[0][0].toUpperCase(); + + const canvas = document.createElement("canvas"); + canvas.width = 100; + canvas.height = 100; + const ctx = canvas.getContext("2d"); + + const colors = [ + "#2196F3", + "#9C27B0", + "#E91E63", + "#009688", + "#4CAF50", + "#FF9800", + ]; + ctx.fillStyle = + colors[ + name.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0) % + colors.length + ]; + ctx.fillRect(0, 0, 100, 100); + + ctx.fillStyle = "#ffffff"; + ctx.font = "bold 40px Arial"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(initials, 50, 50); + + return canvas.toDataURL(); + } + function onWindowResize() { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { diff --git a/src/dialogs/rateBox.js b/src/dialogs/rateBox.js index 86dd85d60..1ec62a8a1 100644 --- a/src/dialogs/rateBox.js +++ b/src/dialogs/rateBox.js @@ -1,4 +1,4 @@ -import constants from "lib/constants"; +import config from "lib/config"; import template from "views/rating.hbs"; import box from "./box"; @@ -34,7 +34,7 @@ function rateBox() { const stars = getStars(val); const subject = "feedback - Acode editor"; const textBody = stars + "
%0A" + getFeedbackBody("
%0A"); - const email = constants.FEEDBACK_EMAIL; + const email = config.FEEDBACK_EMAIL; system.openInBrowser( `mailto:${email}?subject=${subject}&body=${textBody}`, ); diff --git a/src/fileSystem/index.js b/src/fileSystem/index.js index 2d74065ad..7169afdec 100644 --- a/src/fileSystem/index.js +++ b/src/fileSystem/index.js @@ -1,4 +1,4 @@ -import ajax from "@deadlyjack/ajax"; +import ajax from "lib/ajax"; import { decode } from "utils/encodings"; import Url from "utils/Url"; import externalFs from "./externalFs"; diff --git a/src/fileSystem/internalFs.js b/src/fileSystem/internalFs.js index 870da7c41..b26bd585e 100644 --- a/src/fileSystem/internalFs.js +++ b/src/fileSystem/internalFs.js @@ -1,5 +1,5 @@ import fsOperation from "fileSystem"; -import ajax from "@deadlyjack/ajax"; +import ajax from "lib/ajax"; import { decode, encode } from "utils/encodings"; import helpers from "utils/helpers"; import Url from "utils/Url"; diff --git a/src/handlers/editorFileTab.js b/src/handlers/editorFileTab.js index 0b26d1059..a6cc2eaf3 100644 --- a/src/handlers/editorFileTab.js +++ b/src/handlers/editorFileTab.js @@ -1,4 +1,4 @@ -import constants from "lib/constants"; +import config from "lib/config"; import settings from "lib/settings"; const opts = { passive: false }; @@ -82,7 +82,7 @@ export default function startDrag(e) { } if (settings.value.vibrateOnTap) { - navigator.vibrate(constants.VIBRATION_TIME); + navigator.vibrate(config.VIBRATION_TIME); } $tab = e.target; diff --git a/src/handlers/quickToolsInit.js b/src/handlers/quickToolsInit.js index 5c803b7ee..2e3e7fc9c 100644 --- a/src/handlers/quickToolsInit.js +++ b/src/handlers/quickToolsInit.js @@ -1,5 +1,5 @@ import quickTools from "components/quickTools"; -import constants from "lib/constants"; +import config from "lib/config"; import appSettings from "lib/settings"; import actions, { key } from "./quickTools"; @@ -300,7 +300,7 @@ function oncontextmenu(e) { const { editor, activeFile } = editorManager; if (isClickMode && appSettings.value.vibrateOnTap) { - navigator.vibrate(constants.VIBRATION_TIME_LONG); + navigator.vibrate(config.VIBRATION_TIME_LONG); $el.classList.add("active"); } diff --git a/src/index.d.ts b/src/index.d.ts index e5f3190ce..b17d84e69 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -5,7 +5,6 @@ declare const DATA_STORAGE: string; declare const CACHE_STORAGE: string; declare const PLUGIN_DIR: string; declare const KEYBINDING_FILE: string; -declare const IS_FREE_VERSION: string; declare const ANDROID_SDK_INT: number; declare const DOES_SUPPORT_THEME: boolean; declare const acode: object; @@ -16,7 +15,6 @@ interface Window { CACHE_STORAGE: string; PLUGIN_DIR: string; KEYBINDING_FILE: string; - IS_FREE_VERSION: string; ANDROID_SDK_INT: number; DOES_SUPPORT_THEME: boolean; acode: object; diff --git a/src/lang/ar-ye.json b/src/lang/ar-ye.json index badff67b7..6c25d8eb2 100644 --- a/src/lang/ar-ye.json +++ b/src/lang/ar-ye.json @@ -725,5 +725,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/be-by.json b/src/lang/be-by.json index c70f7c03c..39d8f0bbb 100644 --- a/src/lang/be-by.json +++ b/src/lang/be-by.json @@ -727,5 +727,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/bn-bd.json b/src/lang/bn-bd.json index b4f677755..400e89375 100644 --- a/src/lang/bn-bd.json +++ b/src/lang/bn-bd.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/cs-cz.json b/src/lang/cs-cz.json index c2459c344..1912f5036 100644 --- a/src/lang/cs-cz.json +++ b/src/lang/cs-cz.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/de-de.json b/src/lang/de-de.json index de439f5a5..d5bf4e9f4 100644 --- a/src/lang/de-de.json +++ b/src/lang/de-de.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/en-us.json b/src/lang/en-us.json index 431fcfa97..4df375242 100644 --- a/src/lang/en-us.json +++ b/src/lang/en-us.json @@ -726,5 +726,7 @@ "close selected tabs warning": "Are you sure you want to close the selected tabs? You will lose the unsaved changes and this action cannot be reversed.", "close tabs to right": "Close Right", "close tabs to left": "Close Left", - "close other tabs": "Close Others" -} + "close other tabs": "Close Others", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/es-sv.json b/src/lang/es-sv.json index 0e2fd34be..d56fc838a 100644 --- a/src/lang/es-sv.json +++ b/src/lang/es-sv.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/fr-fr.json b/src/lang/fr-fr.json index eaca49f5d..3195bdc4d 100644 --- a/src/lang/fr-fr.json +++ b/src/lang/fr-fr.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/he-il.json b/src/lang/he-il.json index 1d3d487e9..a9d53a5e2 100644 --- a/src/lang/he-il.json +++ b/src/lang/he-il.json @@ -727,5 +727,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/hi-in.json b/src/lang/hi-in.json index c15d7ea3a..e497c170f 100644 --- a/src/lang/hi-in.json +++ b/src/lang/hi-in.json @@ -727,5 +727,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/hu-hu.json b/src/lang/hu-hu.json index 7613823d7..687ec95c4 100644 --- a/src/lang/hu-hu.json +++ b/src/lang/hu-hu.json @@ -726,5 +726,7 @@ "auto close tags": "Címkék automatikus lezárása", "settings-info-editor-auto-close-tags": "HTML, XML, Vue, Angular és PHP-sablonfájlokban a záró címkék automatikus beillesztése.", "ui zoom": "Felhasználói felület nagyítása", - "settings-info-app-ui-zoom": "Szövegek méretezése az Acode teljes felületén." -} + "settings-info-app-ui-zoom": "Szövegek méretezése az Acode teljes felületén.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/id-id.json b/src/lang/id-id.json index ff86cb1c7..4d31c7e42 100644 --- a/src/lang/id-id.json +++ b/src/lang/id-id.json @@ -727,5 +727,7 @@ "auto close tags": "Penutup tag otomatis", "settings-info-editor-auto-close-tags": "Menyisipkan tag penutup di berkas HTML, XML, Vue, Angular, dan templat PHP secara otomatis.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/ir-fa.json b/src/lang/ir-fa.json index 50d85ef67..db45fe6c8 100644 --- a/src/lang/ir-fa.json +++ b/src/lang/ir-fa.json @@ -727,5 +727,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/it-it.json b/src/lang/it-it.json index 114125dc0..5690aac40 100644 --- a/src/lang/it-it.json +++ b/src/lang/it-it.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/ja-jp.json b/src/lang/ja-jp.json index 9dc2d5ec1..d7b6924e6 100644 --- a/src/lang/ja-jp.json +++ b/src/lang/ja-jp.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/ko-kr.json b/src/lang/ko-kr.json index 848b645e1..b90daac07 100644 --- a/src/lang/ko-kr.json +++ b/src/lang/ko-kr.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/ml-in.json b/src/lang/ml-in.json index fd7d4dfe7..578fcd159 100644 --- a/src/lang/ml-in.json +++ b/src/lang/ml-in.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/mm-unicode.json b/src/lang/mm-unicode.json index b0f5b21ea..b4f56df7d 100644 --- a/src/lang/mm-unicode.json +++ b/src/lang/mm-unicode.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/mm-zawgyi.json b/src/lang/mm-zawgyi.json index e6bb03fb0..bcb0ac2c4 100644 --- a/src/lang/mm-zawgyi.json +++ b/src/lang/mm-zawgyi.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/pl-pl.json b/src/lang/pl-pl.json index 847f07f1b..2d9ad571c 100644 --- a/src/lang/pl-pl.json +++ b/src/lang/pl-pl.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/pt-br.json b/src/lang/pt-br.json index eae43272a..863f017dc 100644 --- a/src/lang/pt-br.json +++ b/src/lang/pt-br.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/pu-in.json b/src/lang/pu-in.json index 506523cf6..5d7d9c426 100644 --- a/src/lang/pu-in.json +++ b/src/lang/pu-in.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/ru-ru.json b/src/lang/ru-ru.json index 3eb467537..8f963052b 100644 --- a/src/lang/ru-ru.json +++ b/src/lang/ru-ru.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/tl-ph.json b/src/lang/tl-ph.json index bc33bf689..a06665cbb 100644 --- a/src/lang/tl-ph.json +++ b/src/lang/tl-ph.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/tr-tr.json b/src/lang/tr-tr.json index 161260ac2..56affd96f 100644 --- a/src/lang/tr-tr.json +++ b/src/lang/tr-tr.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/uk-ua.json b/src/lang/uk-ua.json index 02bd215b2..e514fee4a 100644 --- a/src/lang/uk-ua.json +++ b/src/lang/uk-ua.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/uz-uz.json b/src/lang/uz-uz.json index a21eac915..78d67dcc8 100644 --- a/src/lang/uz-uz.json +++ b/src/lang/uz-uz.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/vi-vn.json b/src/lang/vi-vn.json index 8f61d169e..da4ea3928 100644 --- a/src/lang/vi-vn.json +++ b/src/lang/vi-vn.json @@ -727,5 +727,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/zh-cn.json b/src/lang/zh-cn.json index cb4cfa9e0..7f722f3b4 100644 --- a/src/lang/zh-cn.json +++ b/src/lang/zh-cn.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/zh-hant.json b/src/lang/zh-hant.json index e546ab526..00e7043c3 100644 --- a/src/lang/zh-hant.json +++ b/src/lang/zh-hant.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lang/zh-tw.json b/src/lang/zh-tw.json index 14171dbd5..994d683f1 100644 --- a/src/lang/zh-tw.json +++ b/src/lang/zh-tw.json @@ -726,5 +726,7 @@ "auto close tags": "Auto close tags", "settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.", "ui zoom": "UI zoom", - "settings-info-app-ui-zoom": "Scale text across the Acode interface." -} + "settings-info-app-ui-zoom": "Scale text across the Acode interface.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." +} \ No newline at end of file diff --git a/src/lib/acode.js b/src/lib/acode.js index 0845b8d99..9da051240 100644 --- a/src/lib/acode.js +++ b/src/lib/acode.js @@ -7,7 +7,6 @@ import * as cmLint from "@codemirror/lint"; import * as cmSearch from "@codemirror/search"; import * as cmState from "@codemirror/state"; import * as cmView from "@codemirror/view"; -import ajax from "@deadlyjack/ajax"; import * as lezerHighlight from "@lezer/highlight"; import { getRegisteredCommands as listRegisteredCommands, @@ -74,7 +73,7 @@ import encodings, { decode, encode } from "utils/encodings"; import helpers from "utils/helpers"; import KeyboardEvent from "utils/keyboardEvent"; import Url from "utils/Url"; -import constants from "./constants"; +import config from "./config"; export default class Acode { #modules = {}; @@ -522,10 +521,7 @@ export default class Acode { let purchaseToken; let product; - const pluginUrl = Url.join( - constants.API_BASE, - `plugin/${pluginId}`, - ); + const pluginUrl = Url.join(config.API_BASE, `plugin/${pluginId}`); fsOperation(pluginUrl) .readFile("json") .catch(() => { @@ -581,16 +577,14 @@ export default class Acode { async function onpurchase(e) { const purchase = await getPurchase(product.productId); - await ajax.post( - Url.join(constants.API_BASE, "plugin/order"), - { - data: { - id: remotePlugin.id, - token: purchase?.purchaseToken, - package: BuildInfo.packageName, - }, - }, - ); + await fetch(Url.join(config.API_BASE, "plugin/order"), { + method: "POST", + body: JSON.stringify({ + id: remotePlugin.id, + token: purchase?.purchaseToken, + package: BuildInfo.packageName, + }), + }); purchaseToken = purchase?.purchaseToken; } diff --git a/src/lib/adRewards.js b/src/lib/adRewards.js index 0907b3a6b..ef3d94420 100644 --- a/src/lib/adRewards.js +++ b/src/lib/adRewards.js @@ -1,5 +1,6 @@ import toast from "components/toast"; import auth from "./auth"; +import config from "./config"; import secureAdRewardState from "./secureAdRewardState"; const ONE_HOUR = 60 * 60 * 1000; @@ -178,15 +179,7 @@ function scheduleExpiryCheck() { async function getRewardIdentity() { try { - const user = await auth.getUserInfo(); - const userId = - user?.id || - user?._id || - user?.github || - user?.username || - device?.uuid || - "guest"; - return String(userId); + return String(user?.id || "Guest"); } catch (error) { console.warn("Failed to resolve rewarded ad user identity.", error); return String(device?.uuid || "guest"); @@ -323,18 +316,18 @@ export default { return Boolean(state.isActive && state.adFreeUntil > Date.now()); }, canShowAds() { - return Boolean(window.IS_FREE_VERSION && !this.isAdFreeActive()); + return Boolean(!config.HAS_PRO && !this.isAdFreeActive()); }, isRewardedSupported() { - return Boolean( - window.IS_FREE_VERSION && admob?.RewardedAd && getRewardedUnitId(), - ); + return Boolean(!config.HAS_PRO && admob?.RewardedAd && getRewardedUnitId()); }, getRewardedUnavailableReason() { - if (!window.IS_FREE_VERSION) + if (config.HAS_PRO) { return "Ads are already disabled on this build."; - if (!admob?.RewardedAd) + } + if (!admob?.RewardedAd) { return "Rewarded ads are unavailable on this device."; + } if (!getRewardedUnitId()) { return "Rewarded ads are not configured for production yet."; } diff --git a/src/lib/ajax.js b/src/lib/ajax.js new file mode 100644 index 000000000..312c6e614 --- /dev/null +++ b/src/lib/ajax.js @@ -0,0 +1,246 @@ +const GET = "GET"; +const POST = "POST"; +const PATCH = "PATCH"; +const PUT = "PUT"; +const DELETE = "DELETE"; +const PURGE = "PURGE"; + +let xhrs = []; + +/** + * @typedef {Object} AjaxOptions + * @property {string} [contentType="application/json"] - Value of the `Content-Type` request header. + * @property {XMLHttpRequestResponseType} [responseType="json"] - Expected response type. Falls back to `ajax.responseType` if set. + * @property {"GET"|"POST"|"PUT"|"PATCH"|"DELETE"|"PURGE"} [method="GET"] - HTTP method to use for the request. + * @property {(loaded: number, total: number) => void} [onprogress] - Called during upload/download progress with bytes loaded and total. + * @property {(response: *) => void} [onsuccess] - Called when the request completes with a 2xx status. + * @property {(response: XMLHttpRequest | *) => void} [onerror] - Called when the request fails or returns a non-2xx status. + * @property {(event: ProgressEvent) => void} [onload] - Called when the request finishes loading (mirrors `xhr.onload`). + * @property {(event: ProgressEvent) => void} [onloadend] - Called when the request completes, regardless of success or failure. + * @property {(event: ProgressEvent) => void} [onabort] - Called if the request is aborted. + * @property {(event: ProgressEvent) => void} [ontimeout] - Called if the request times out. + * @property {(xhr: XMLHttpRequest) => void} [configure] - Per-request XHR configuration hook, called just before `xhr.send()`. + * @property {string} [mimeType="text/xml"] - Overrides the MIME type returned by the server via `xhr.overrideMimeType()`. + * @property {Object|*} [data] - Request payload. Serialized to JSON automatically when `contentType` is `"application/json"`. + * @property {string} url - URL to send the request to. + */ + +/** + * Sends an HTTP request via `XMLHttpRequest` and returns a `Promise` that resolves + * with the mapped response on success, or rejects with the XHR instance (or mapped + * response) on failure. + * + * Global behaviour can be customised via {@link ajax.configure}, {@link ajax.response}, + * and {@link ajax.onprogress}. + * + * @param {AjaxOptions} [options={}] - Request configuration. + * @returns {Promise<*>} Resolves with the value returned by {@link ajax.response}, + * or rejects with the XHR instance / mapped error response. + * + * @example + * // Basic JSON POST + * const user = await ajax({ + * url: "/api/users", + * method: "POST", + * data: { name: "Alice" }, + * onsuccess: (res) => console.log("Created:", res), + * onerror: (res) => console.error("Failed:", res), + * }); + */ +export default function ajax(options = {}) { + const xhr = getHTTP(); + + const { + contentType = "application/json", + responseType = ajax.responseType || "json", + method = GET, + onprogress = () => {}, + onsuccess = () => {}, + onerror = () => {}, + onload = () => {}, + onloadend = () => {}, + onabort = () => {}, + ontimeout = () => {}, + configure = () => {}, + mimeType = "text/xml", + data, + url, + } = options; + + return new Promise((resolve, reject) => { + let body; + + if (data && contentType === "application/json") { + body = JSON.stringify(data); + } + + xhr.addEventListener("load", onload); + xhr.addEventListener("abort", onabort); + xhr.addEventListener("loadend", onloadend); + xhr.addEventListener("timeout", ontimeout); + xhr.addEventListener("progress", progress); + xhr.addEventListener("error", handleError); + xhr.addEventListener("readystatechange", onreadystatechange); + + xhr.open(method, url, true); + xhr.setRequestHeader("Content-Type", contentType); + xhr.overrideMimeType(mimeType); + ajax.configure(xhr, url); + configure(xhr); + xhr.send(body); + + function onreadystatechange() { + const { readyState, status } = xhr; + + if (readyState === 2) { + if (status >= 200 && status < 300) { + xhr.responseType = responseType; + } else { + xhr.responseType = "text"; + } + } else if (readyState === 4) { + if (status >= 200 && status < 300) { + const res = ajax.response(xhr); + onsuccess(res); + resolve(res); + } else { + handleError(); + } + } + } + + function progress(e) { + const { loaded, total } = e; + const percent = Math.round((loaded / total) * 100); + xhr.percent = percent; + + if (typeof onprogress === "function") { + onprogress(loaded, total); + } + + if (typeof ajax.onprogress === "function") { + const progresses = []; + xhrs = xhrs.filter((xhr) => { + if (xhr.status !== 200 || xhr.percent === 100) return false; + progresses.push(xhr.percent); + return true; + }); + + ajax.onprogress(Math.min(...progresses, 100)); + } + } + + function handleError() { + let res = xhr; + + if (responseType === "json") { + let json; + + try { + json = JSON.parse(xhr.responseText); + } catch (error) { + json = xhr.responseText; + } + + Object.defineProperty(xhr, "response", { + value: json, + }); + } + + if (typeof ajax.response === "function") { + res = ajax.response(xhr); + } + + onerror(res); + reject(res); + } + }); + + /** + * @returns {XMLHttpRequest} + */ + function getHTTP() { + const xhr = new XMLHttpRequest(); + xhrs.push(xhr); + return xhr; + } +} + +/** + * Global response mapper applied to every completed request (success **and** error). + * + * Override this to normalise or unwrap XHR responses application-wide. + * The return value becomes the resolved/rejected value of the `ajax()` promise. + * + * @param {XMLHttpRequest} xhr - The completed XHR instance. + * @returns {*} The value that the promise will resolve or reject with. + * + * @example + * ajax.response = (xhr) => xhr.response?.data ?? xhr.response; + */ +ajax.response = (xhr) => {}; + +/** + * Global XHR configuration hook called on every request, just before `xhr.send()`. + * + * Use this to attach auth headers, CSRF tokens, or any other cross-cutting concerns + * without repeating them in every call site. + * + * @param {XMLHttpRequest} xhr - The XHR instance about to be sent. + * @param {string} url + * + * @example + * ajax.configure = (xhr) => { + * xhr.setRequestHeader("Authorization", `Bearer ${getToken()}`); + * xhr.timeout = 30_000; + * }; + */ +ajax.configure = (xhr, url) => {}; + +ajax.get = function (url, options = {}) { + return ajax({ + url, + method: GET, + ...options, + }); +}; + +ajax.post = function (url, options = {}) { + return ajax({ + url, + method: POST, + ...options, + }); +}; + +ajax.put = function (url, options = {}) { + return ajax({ + url, + method: PUT, + ...options, + }); +}; + +ajax.patch = function (url, options = {}) { + return ajax({ + url, + method: PATCH, + ...options, + }); +}; + +ajax.delete = function (url, options = {}) { + return ajax({ + url, + method: DELETE, + ...options, + }); +}; + +ajax.purge = function (url, options = {}) { + return ajax({ + url, + method: PURGE, + ...options, + }); +}; diff --git a/src/lib/applySettings.js b/src/lib/applySettings.js index 8a76c2360..daaeab924 100644 --- a/src/lib/applySettings.js +++ b/src/lib/applySettings.js @@ -1,7 +1,7 @@ import actions from "../handlers/quickTools"; import appSettings from "../lib/settings"; import themes from "../theme/list"; -import constants from "./constants"; +import config from "./config"; import fonts from "./fonts"; export default { @@ -18,7 +18,7 @@ export default { app.addEventListener("click", function (e) { const $target = e.target; if ($target.hasAttribute("vibrate") && appSettings.value.vibrateOnTap) { - navigator.vibrate(constants.VIBRATION_TIME); + navigator.vibrate(config.VIBRATION_TIME); } }); diff --git a/src/lib/auth.js b/src/lib/auth.js index c8911be88..23d7ad594 100644 --- a/src/lib/auth.js +++ b/src/lib/auth.js @@ -1,5 +1,30 @@ import toast from "components/toast"; import { addIntentHandler } from "handlers/intent"; +import config from "./config"; + +/** + * @typedef {object} User + * @property {number} id + * @property {string} name + * @property {string} role + * @property {string} email + * @property {string} github + * @property {string} website + * @property {number} verified + * @property {number} threshold + * @property {number} acode_pro + * @property {string} pro_purchased_at + * @property {string} created_at + * @property {string} updated_at + * @property {boolean} isAdmin + */ + +/**@type {User|null} */ +let loggedInUser = null; +/**@type {number} */ +let cacheTimeout = null; + +const CACHE_USER_KEY = "cached-logged-in-user"; const loginEvents = { listeners: new Set(), @@ -25,7 +50,7 @@ class AuthService { try { if (event?.module === "user" && event?.action === "login") { if (event?.value) { - this._exec("saveToken", [event.value]); + this.#exec("saveToken", [event.value]); toast("Logged in successfully"); setTimeout(() => { @@ -43,112 +68,70 @@ class AuthService { /** * Helper to wrap cordova.exec in a Promise */ - _exec(action, args = []) { + #exec(action, args = []) { return new Promise((resolve, reject) => { cordova.exec(resolve, reject, "Authenticator", action, args); }); } - async openLoginUrl() { - const url = "https://acode.app/login?redirect=app"; - + async logout() { try { - await new Promise((resolve, reject) => { - CustomTabs.open(url, { showTitle: true }, resolve, reject); + const res = await fetch(`${config.API_BASE}/login`, { + method: "DELETE", }); + if (!res.ok) { + throw new Error("Unable to logout."); + } } catch (error) { - console.error("CustomTabs failed, opening system browser.", error); - system.openInBrowser(url); + console.error("Error during logout:", error); } - } - async logout() { - try { - await this._exec("logout"); - return true; - } catch (error) { - console.error("Failed to logout.", error); - return false; - } - } + loggedInUser = null; + localStorage.removeItem(CACHE_USER_KEY); - async isLoggedIn() { try { - // Native checks EncryptedPrefs and validates with API internally - await this._exec("isLoggedIn"); + await this.#exec("logout"); return true; } catch (error) { - console.error(error); - // error is typically the status code (0 if no token, 401 if invalid) + console.error("Failed to logout.", error); return false; } } - async getUserInfo() { - try { - const data = await this._exec("getUserInfo"); - return typeof data === "string" ? JSON.parse(data) : data; - } catch (error) { - console.error("Failed to fetch user data.", error); - return null; - } - } + /** + * + * @returns {User} + */ + async getLoggedInUser() { + if (loggedInUser) return loggedInUser; - async getAvatar() { try { - const userData = await this.getUserInfo(); - if (!userData) return null; - - if (userData.github) { - return `https://avatars.githubusercontent.com/${userData.github}`; + const res = await fetch(`${config.API_BASE}/login`); + + if (res.ok) { + loggedInUser = await res.json(); + localStorage.setItem(CACHE_USER_KEY, JSON.stringify(loggedInUser)); + clearTimeout(cacheTimeout); + cacheTimeout = setTimeout(() => (loggedInUser = null), 600_000); + return loggedInUser; } - if (userData.name) { - return this._generateInitialsAvatar(userData.name); + if (res.status === 401) { + localStorage.removeItem(CACHE_USER_KEY); + return null; } - return null; + throw new Error("Unable to fetch user Info"); } catch (error) { - console.error("Failed to get avatar", error); - return null; + if (CACHE_USER_KEY in localStorage) { + try { + return JSON.parse(localStorage.getItem(CACHE_USER_KEY)); + } catch {} + } + toast("Unable to fetch user info"); + throw error; } } - - _generateInitialsAvatar(name) { - const nameParts = name.split(" "); - const initials = - nameParts.length >= 2 - ? `${nameParts[0][0]}${nameParts[1][0]}`.toUpperCase() - : nameParts[0][0].toUpperCase(); - - const canvas = document.createElement("canvas"); - canvas.width = 100; - canvas.height = 100; - const ctx = canvas.getContext("2d"); - - const colors = [ - "#2196F3", - "#9C27B0", - "#E91E63", - "#009688", - "#4CAF50", - "#FF9800", - ]; - ctx.fillStyle = - colors[ - name.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0) % - colors.length - ]; - ctx.fillRect(0, 0, 100, 100); - - ctx.fillStyle = "#ffffff"; - ctx.font = "bold 40px Arial"; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(initials, 50, 50); - - return canvas.toDataURL(); - } } export default new AuthService(); diff --git a/src/lib/checkPluginsUpdate.js b/src/lib/checkPluginsUpdate.js index f58dee687..58d89dc14 100644 --- a/src/lib/checkPluginsUpdate.js +++ b/src/lib/checkPluginsUpdate.js @@ -1,6 +1,6 @@ -import ajax from "@deadlyjack/ajax"; -import fsOperation from "../fileSystem"; -import Url from "../utils/Url"; +import fsOperation from "fileSystem"; +import Url from "utils/Url"; +import config from "./config"; export default async function checkPluginsUpdate() { const plugins = await fsOperation(PLUGIN_DIR).lsDir(); @@ -14,12 +14,15 @@ export default async function checkPluginsUpdate() { Url.join(pluginDir.url, "plugin.json"), ).readFile("json"); - const res = await ajax({ - url: `https://acode.app/api/plugin/check-update/${plugin.id}/${plugin.version}`, - }); + const res = await fetch( + `${config.API_BASE}/plugin/check-update/${plugin.id}/${plugin.version}`, + ); - if (res.update) { - updates.push(plugin.id); + if (res.ok) { + const json = await res.json(); + if (json.update) { + updates.push(plugin.id); + } } })(), ); diff --git a/src/lib/commands.js b/src/lib/commands.js index 46714a492..96fa6c0d9 100644 --- a/src/lib/commands.js +++ b/src/lib/commands.js @@ -26,7 +26,7 @@ import { getColorRange } from "utils/color/regex"; import helpers from "utils/helpers"; import Url from "utils/Url"; import checkFiles from "./checkFiles"; -import constants from "./constants"; +import config from "./config"; import EditorFile from "./editorFile"; import openFile from "./openFile"; import openFolder from "./openFolder"; @@ -228,7 +228,7 @@ export default { }, async "new-file"() { let filename = await prompt(strings["enter file name"], "", "filename", { - match: constants.FILE_NAME_REGEX, + match: config.FILE_NAME_REGEX, required: true, }); @@ -494,7 +494,7 @@ export default { } let newname = await prompt(strings.rename, file.filename, "filename", { - match: constants.FILE_NAME_REGEX, + match: config.FILE_NAME_REGEX, capitalize: false, }); @@ -548,7 +548,7 @@ export default { editorManager.activeFile.eol = eol; }, "open-log-file"() { - openFile(Url.join(DATA_STORAGE, constants.LOG_FILE_NAME)); + openFile(Url.join(DATA_STORAGE, config.LOG_FILE_NAME)); }, "copy-device-info"() { let webviewInfo = {}; diff --git a/src/lib/constants.js b/src/lib/config.js similarity index 84% rename from src/lib/constants.js rename to src/lib/config.js index f1315999a..55e444506 100644 --- a/src/lib/constants.js +++ b/src/lib/config.js @@ -1,4 +1,9 @@ +const BASE_URL = "https://acode.app"; +let hasPro = false; + export default { + BASE_URL, + SUPPORTED_EDITOR: "cm", FILE_NAME_REGEX: /^((?![:<>"\\\|\?\*]).)*$/, FONT_SIZE: /^[0-9\.]{1,3}(px|rem|em|pt|mm|pc|in)$/, DEFAULT_FILE_SESSION: "default-session", @@ -19,18 +24,23 @@ export default { get PLAY_STORE_URL() { return `https://play.google.com/store/apps/details?id=${BuildInfo.packageName}`; }, - API_BASE: "https://acode.app/api", - // API_BASE: 'https://192.168.0.102:3001/api', // test api + API_BASE: `${BASE_URL}/api`, SKU_LIST: ["crystal", "bronze", "silver", "gold", "platinum", "titanium"], LOG_FILE_NAME: "Acode.log", // Social Links DOCS_URL: "https://docs.acode.app", - WEBSITE_URL: "https://acode.app", GITHUB_URL: "https://github.com/Acode-Foundation/Acode", TELEGRAM_URL: "https://t.me/foxdebug_acode", DISCORD_URL: "https://discord.gg/nDqZsh7Rqz", TWITTER_URL: "https://x.com/foxbiz_io", INSTAGRAM_URL: "https://www.instagram.com/foxbiz.io/", FOXBIZ_URL: "https://foxbiz.io", + + get HAS_PRO() { + return hasPro; + }, + set HAS_PRO(value) { + hasPro = value; + }, }; diff --git a/src/lib/devTools.js b/src/lib/devTools.js index 8b7077aee..8698b6677 100644 --- a/src/lib/devTools.js +++ b/src/lib/devTools.js @@ -1,9 +1,8 @@ import fsOperation from "fileSystem"; -import ajax from "@deadlyjack/ajax"; import loader from "dialogs/loader"; import helpers from "utils/helpers"; import Url from "utils/Url"; -import constants from "./constants"; +import config from "./config"; let erudaInstance = null; let isInitialized = false; @@ -50,11 +49,9 @@ const devTools = { } try { - const erudaScript = await ajax({ - url: constants.ERUDA_CDN, - responseType: "text", - contentType: "application/x-www-form-urlencoded", - }); + const erudaScript = await fsOperation(config.ERUDA_CDN).readFile( + "utf-8", + ); await fsOperation(DATA_STORAGE).createFile("eruda.js", erudaScript); } finally { if (showLoader) loader.destroy(); diff --git a/src/lib/editorFile.js b/src/lib/editorFile.js index eddb71cb3..2d3be54e1 100644 --- a/src/lib/editorFile.js +++ b/src/lib/editorFile.js @@ -20,7 +20,7 @@ import mimeTypes from "mime-types"; import helpers from "utils/helpers"; import Path from "utils/Path"; import Url from "utils/Url"; -import constants from "./constants"; +import config from "./config"; import openFolder from "./openFolder"; import run from "./run"; import saveFile from "./saveFile"; @@ -312,7 +312,7 @@ export default class EditorFile { * Name of the file * @type {string} */ - #name = constants.DEFAULT_FILE_NAME; + #name = config.DEFAULT_FILE_NAME; /** * Location of the file * @type {string} @@ -322,7 +322,7 @@ export default class EditorFile { * Unique ID of the file, changed when file is renamed or location/uri is changed. * @type {string} */ - #id = constants.DEFAULT_FILE_SESSION; + #id = config.DEFAULT_FILE_SESSION; /** * Associated tile for the file, that is append in the open file list, * when clicked make the file active. @@ -407,7 +407,7 @@ export default class EditorFile { } else this.#id = options.id; } else if (!options) { // if options aren't passed, that means default file is being created - this.#id = constants.DEFAULT_FILE_SESSION; + this.#id = config.DEFAULT_FILE_SESSION; } if (options?.type) { @@ -500,7 +500,7 @@ export default class EditorFile { // if options contains text property then there is no need to load // set loaded true - if (this.#id !== constants.DEFAULT_FILE_SESSION) { + if (this.#id !== config.DEFAULT_FILE_SESSION) { this.loaded = options?.text !== undefined; } @@ -623,7 +623,7 @@ export default class EditorFile { if (event.defaultPrevented) return; (async () => { - if (this.id === constants.DEFAULT_FILE_SESSION) { + if (this.id === config.DEFAULT_FILE_SESSION) { this.id = helpers.uuid(); } @@ -862,7 +862,7 @@ export default class EditorFile { // here readonly means file has uri but has no write permission. if (!this.uri || this.readOnly) { // if file is default file and text is changed - if (this.id === constants.DEFAULT_FILE_SESSION) { + if (this.id === config.DEFAULT_FILE_SESSION) { // change id when text is changed this.id = helpers.uuid(); } @@ -983,10 +983,7 @@ export default class EditorFile { async remove(force = false, options = {}) { const { ignorePinned = false, silentPinned = false } = options || {}; - if ( - this.id === constants.DEFAULT_FILE_SESSION && - !editorManager.files.length - ) + if (this.id === config.DEFAULT_FILE_SESSION && !editorManager.files.length) return false; if (this.pinned && !ignorePinned) { if (!silentPinned) { @@ -1207,9 +1204,9 @@ export default class EditorFile { render() { this.makeActive(); - if (this.id !== constants.DEFAULT_FILE_SESSION) { + if (this.id !== config.DEFAULT_FILE_SESSION) { const defaultFile = editorManager.getFile( - constants.DEFAULT_FILE_SESSION, + config.DEFAULT_FILE_SESSION, "id", ); defaultFile?.remove(); diff --git a/src/lib/installPlugin.js b/src/lib/installPlugin.js index 5488d9131..335c6e7ed 100644 --- a/src/lib/installPlugin.js +++ b/src/lib/installPlugin.js @@ -1,5 +1,4 @@ import fsOperation from "fileSystem"; -import ajax from "@deadlyjack/ajax"; import alert from "dialogs/alert"; import confirm from "dialogs/confirm"; import loader from "dialogs/loader"; @@ -7,7 +6,7 @@ import purchaseListener from "handlers/purchase"; import JSZip from "jszip"; import helpers from "utils/helpers"; import Url from "utils/Url"; -import constants from "./constants"; +import config from "./config"; import InstallState from "./installState"; import loadPlugin from "./loadPlugin"; @@ -50,7 +49,7 @@ export default async function installPlugin( if (!/^(https?|file|content):/.test(id)) { pluginUrl = Url.join( - constants.API_BASE, + config.API_BASE, "plugin/download/", `${id}?device=${device.uuid}`, ); @@ -68,7 +67,7 @@ export default async function installPlugin( let plugin; if ( - pluginUrl.includes(constants.API_BASE) || + pluginUrl.includes(config.API_BASE) || pluginUrl.startsWith("file:") || pluginUrl.startsWith("content:") ) { @@ -397,7 +396,7 @@ async function resolveDepsManifest(deps) { const resolved = []; for (const dependency of deps) { const remoteDependency = await fsOperation( - constants.API_BASE, + config.API_BASE, `plugin/${dependency}`, ) .readFile("json") @@ -467,12 +466,13 @@ async function resolveDep(manifest) { async function onpurchase(e) { const purchase = await getPurchase(product.productId); - await ajax.post(Url.join(constants.API_BASE, "plugin/order"), { - data: { + await fetch(Url.join(config.API_BASE, "plugin/order"), { + method: "POST", + body: JSON.stringify({ id: manifest.id, token: purchase?.purchaseToken, package: BuildInfo.packageName, - }, + }), }); purchaseToken = purchase?.purchaseToken; } diff --git a/src/lib/logger.js b/src/lib/logger.js index a2e8d05c6..48c58b9fa 100644 --- a/src/lib/logger.js +++ b/src/lib/logger.js @@ -1,6 +1,6 @@ import fsOperation from "fileSystem"; import Url from "utils/Url"; -import constants from "./constants"; +import config from "./config"; /* /** @@ -33,7 +33,7 @@ class Logger { this.#logBuffer = new Map(); this.#maxBufferSize = maxBufferSize; this.#logLevel = logLevel; - this.#logFileName = constants.LOG_FILE_NAME; + this.#logFileName = config.LOG_FILE_NAME; this.#flushInterval = flushInterval; this.#maxFileSize = maxFileSize; this.#startAutoFlush(); // Automatically flush logs at intervals @@ -76,10 +76,10 @@ class Logger { #writeLogToFile = async (logContent) => { try { - const logFilePath = Url.join(DATA_STORAGE, constants.LOG_FILE_NAME); + const logFilePath = Url.join(DATA_STORAGE, config.LOG_FILE_NAME); if (!(await fsOperation(logFilePath).exists())) { await fsOperation(window.DATA_STORAGE).createFile( - constants.LOG_FILE_NAME, + config.LOG_FILE_NAME, logContent, ); } else { diff --git a/src/lib/openFolder.js b/src/lib/openFolder.js index 11add0d1a..ed177c6f7 100644 --- a/src/lib/openFolder.js +++ b/src/lib/openFolder.js @@ -16,7 +16,7 @@ import helpers from "utils/helpers"; import Path from "utils/Path"; import Uri from "utils/Uri"; import Url from "utils/Url"; -import constants from "./constants"; +import config from "./config"; import * as FileList from "./fileList"; import openFile from "./openFile"; import recents from "./recents"; @@ -331,7 +331,7 @@ function handleItems(e) { */ async function handleContextmenu(type, url, name, $target) { if (appSettings.value.vibrateOnTap) { - navigator.vibrate(constants.VIBRATION_TIME); + navigator.vibrate(config.VIBRATION_TIME); } const { clipBoard, $node } = openFolder.find(url); const cancel = `${strings.cancel}${clipBoard ? ` (${strings[clipBoard.action]})` : ""}`; @@ -632,7 +632,7 @@ function execOperation(type, action, url, $target, name) { return; } let newName = await prompt(strings.rename, name, "text", { - match: constants.FILE_NAME_REGEX, + match: config.FILE_NAME_REGEX, required: true, }); @@ -683,7 +683,7 @@ function execOperation(type, action, url, $target, name) { : strings["enter folder name"]; let newName = await prompt(msg, "", "text", { - match: constants.FILE_NAME_REGEX, + match: config.FILE_NAME_REGEX, required: true, }); diff --git a/src/lib/polyfill.js b/src/lib/polyfill.js index 9a853d5bc..3ed91597b 100644 --- a/src/lib/polyfill.js +++ b/src/lib/polyfill.js @@ -1,3 +1,14 @@ +// automatically include credentials for acode.app API requests +(function () { + const _fetch = window.fetch; + window.fetch = function (url, options) { + if (typeof url === "string" && url.includes("acode.app/api")) { + options = { ...options, credentials: "include" }; + } + return _fetch.call(this, url, options); + }; +})(); + // polyfill for prepend (function (arr) { diff --git a/src/lib/removeAds.js b/src/lib/removeAds.js index 5b65f383e..a92c03a5d 100644 --- a/src/lib/removeAds.js +++ b/src/lib/removeAds.js @@ -1,4 +1,5 @@ import purchaseListener from "handlers/purchase"; +import config from "./config.js"; import { hideAd } from "./startAd.js"; /** @@ -27,7 +28,7 @@ export default function removeAds() { resolve(null); hideAd(true); localStorage.setItem("acode_pro", "true"); - window.IS_FREE_VERSION = false; + config.HAS_PRO = true; toast(strings["thank you :)"]); } }); diff --git a/src/lib/restoreTheme.js b/src/lib/restoreTheme.js index 8c88650a6..e389cfb5b 100644 --- a/src/lib/restoreTheme.js +++ b/src/lib/restoreTheme.js @@ -1,5 +1,6 @@ import themes from "theme/list"; import Color from "utils/color"; +import config from "./config"; import appSettings from "./settings"; let count = 0; @@ -19,7 +20,7 @@ export default function restoreTheme(darken = false) { let themeName = DOES_SUPPORT_THEME ? appSettings.value.appTheme : "default"; let theme = themes.get(themeName); - if (theme?.version !== "free" && IS_FREE_VERSION) { + if (theme?.version !== "free" && !config.HAS_PRO) { themeName = "default"; theme = themes.get(themeName); appSettings.value.appTheme = themeName; diff --git a/src/lib/run.js b/src/lib/run.js index a0eac7c42..bf8862a0f 100644 --- a/src/lib/run.js +++ b/src/lib/run.js @@ -13,7 +13,7 @@ import helpers from "utils/helpers"; import Url from "utils/Url"; import $_console from "views/console.hbs"; import $_markdown from "views/markdown.hbs"; -import constants from "./constants"; +import config from "./config"; import EditorFile from "./editorFile"; import openFolder from "./openFolder"; import appSettings from "./settings"; @@ -142,7 +142,7 @@ async function run( target = "inapp"; filename = "console.html"; pathName = `${ASSETS_DIRECTORY}www/`; - port = constants.CONSOLE_PORT; + port = config.CONSOLE_PORT; } function start() { diff --git a/src/lib/saveFile.js b/src/lib/saveFile.js index 01c8d01db..83a89cdae 100644 --- a/src/lib/saveFile.js +++ b/src/lib/saveFile.js @@ -5,7 +5,7 @@ import recents from "lib/recents"; import FileBrowser from "pages/fileBrowser"; import helpers from "utils/helpers"; import Url from "utils/Url"; -import constants from "./constants"; +import config from "./config"; import EditorFile from "./editorFile"; import openFolder from "./openFolder"; import appSettings from "./settings"; @@ -165,7 +165,7 @@ async function saveFile(file, isSaveAs = false) { name || "", strings["new file"], { - match: constants.FILE_NAME_REGEX, + match: config.FILE_NAME_REGEX, required: true, }, ); diff --git a/src/lib/saveState.js b/src/lib/saveState.js index a6ba3c1d4..c442d1f7a 100644 --- a/src/lib/saveState.js +++ b/src/lib/saveState.js @@ -1,5 +1,5 @@ import { getAllFolds, getScrollPosition, getSelection } from "cm/editorUtils"; -import constants from "./constants"; +import config from "./config"; import { addedFolder } from "./openFolder"; import appSettings from "./settings"; @@ -13,7 +13,7 @@ export default () => { files.forEach((file) => { if (file.type !== "editor") return; - if (file.id === constants.DEFAULT_FILE_SESSION) return; + if (file.id === config.DEFAULT_FILE_SESSION) return; if (file.SAFMode === "single") return; // Selection per file: diff --git a/src/lib/settings.js b/src/lib/settings.js index 42b161921..b092daf40 100644 --- a/src/lib/settings.js +++ b/src/lib/settings.js @@ -4,7 +4,7 @@ import themes from "theme/list"; import { getSystemEditorTheme } from "theme/preInstalled"; import helpers from "utils/helpers"; import Url from "utils/Url"; -import constants from "./constants"; +import config from "./config"; import lang from "./lang"; import { isDeviceDarkTheme } from "./systemConfiguration"; @@ -119,8 +119,8 @@ class Settings { formatter: {}, prettier: {}, maxFileSize: 12, - serverPort: constants.SERVER_PORT, - previewPort: constants.PREVIEW_PORT, + serverPort: config.SERVER_PORT, + previewPort: config.PREVIEW_PORT, showConsoleToggler: true, previewMode: this.PREVIEW_MODE_INAPP, disableCache: false, @@ -165,7 +165,7 @@ class Settings { rememberFolders: true, diagonalScrolling: false, reverseScrolling: false, - scrollSpeed: constants.SCROLL_SPEED_NORMAL, + scrollSpeed: config.SCROLL_SPEED_NORMAL, customTheme: this.#customTheme, relativeLineNumbers: false, elasticTabstops: false, diff --git a/src/lib/startAd.js b/src/lib/startAd.js index 7afc255a1..da509637f 100644 --- a/src/lib/startAd.js +++ b/src/lib/startAd.js @@ -1,4 +1,5 @@ import tag from "html-tag-js"; +import config from "./config"; let adUnitIdBanner = "ca-app-pub-5911839694379275/9157899592"; // Production let adUnitIdInterstitial = "ca-app-pub-5911839694379275/9570937608"; // Production @@ -6,7 +7,7 @@ let adUnitIdRewarded = "ca-app-pub-5911839694379275/1633667633"; // Production let initialized = false; export default async function startAd() { - if (!IS_FREE_VERSION || !admob) return; + if (config.HAS_PRO || !admob) return; if (!initialized) { initialized = true; diff --git a/src/main.js b/src/main.js index 65067364a..ee73ac998 100644 --- a/src/main.js +++ b/src/main.js @@ -13,7 +13,6 @@ import "components/WebComponents"; import fsOperation from "fileSystem"; import sidebarApps from "sidebarApps"; -import ajax from "@deadlyjack/ajax"; import { setKeyBindings } from "cm/commandRegistry"; import { getModeForPath, @@ -36,9 +35,11 @@ import windowResize from "handlers/windowResize"; import Acode from "lib/acode"; import actionStack from "lib/actionStack"; import adRewards from "lib/adRewards"; +import ajax from "lib/ajax"; import applySettings from "lib/applySettings"; import checkFiles from "lib/checkFiles"; import checkPluginsUpdate from "lib/checkPluginsUpdate"; +import config from "lib/config"; import EditorFile from "lib/editorFile"; import EditorManager from "lib/editorManager"; import { initFileList } from "lib/fileList"; @@ -130,6 +131,12 @@ async function Main() { return xhr.response; }; + ajax.configure = (xhr, url) => { + if (url.includes("acode.app/api")) { + xhr.withCredentials = true; + } + }; + loadPolyFill.apply(window); TouchEvent.prototype.preventDefault = function () { @@ -169,9 +176,10 @@ async function onDeviceReady() { window.CACHE_STORAGE = externalCacheDirectory || cacheDirectory; window.PLUGIN_DIR = Url.join(DATA_STORAGE, "plugins"); window.KEYBINDING_FILE = Url.join(DATA_STORAGE, ".key-bindings.json"); - window.IS_FREE_VERSION = isFreePackage; window.log = logger.log.bind(logger); + config.HAS_PRO = !isFreePackage; + // Capture synchronous errors window.addEventListener("error", (event) => { const errorMsg = `Error: ${event.message}, Source: ${event.filename}, Line: ${event.lineno}, Column: ${event.colno}, Stack: ${event.error?.stack || "N/A"}`; @@ -194,7 +202,7 @@ async function onDeviceReady() { }); if (localStorage.acode_pro === "true") { - window.IS_FREE_VERSION = false; + config.HAS_PRO = true; } if (navigator.onLine) { @@ -203,9 +211,9 @@ async function onDeviceReady() { p.productIds.includes("acode_pro_new"), ); if (isPro) { - window.IS_FREE_VERSION = false; + config.HAS_PRO = true; } else { - window.IS_FREE_VERSION = isFreePackage; + config.HAS_PRO = !isFreePackage; } } } catch (error) { @@ -332,8 +340,11 @@ async function onDeviceReady() { // Check login status before emitting events try { - const isLoggedIn = await auth.isLoggedIn(); - if (isLoggedIn) { + const user = await auth.getLoggedInUser(); + if (user) { + if (Boolean(user.acode_pro)) { + config.HAS_PRO = true; + } loginEvents.emit(); } } catch (error) { diff --git a/src/pages/about/about.js b/src/pages/about/about.js index 374e564c3..244d97c0e 100644 --- a/src/pages/about/about.js +++ b/src/pages/about/about.js @@ -3,7 +3,7 @@ import Logo from "components/logo"; import Page from "components/page"; import Reactive from "html-tag-js/reactive"; import actionStack from "lib/actionStack"; -import constants from "lib/constants"; +import config from "lib/config"; import { hideAd } from "lib/startAd"; import helpers from "utils/helpers"; export default function AboutInclude() { @@ -42,22 +42,22 @@ export default function AboutInclude() {
{webviewPackageName}
- +
Official webpage -
{constants.WEBSITE_URL}
+
{config.BASE_URL}
- +
Foxbiz Software Pvt. Ltd. -
{constants.FOXBIZ_URL}
+
{config.FOXBIZ_URL}
@@ -69,31 +69,31 @@ export default function AboutInclude() { Mail - +
Twitter
- +
Instagram
- +
GitHub
- +
Telegram
- +
diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index 24408c007..f0b9a5d02 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -14,7 +14,7 @@ import select from "dialogs/select"; import JSZip from "jszip"; import actionStack from "lib/actionStack"; import checkFiles from "lib/checkFiles"; -import constants from "lib/constants"; +import config from "lib/config"; import openFolder from "lib/openFolder"; import projects from "lib/projects"; import recents from "lib/recents"; @@ -762,7 +762,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { async function contextMenuHandler() { if (appSettings.value.vibrateOnTap) { - navigator.vibrate(constants.VIBRATION_TIME); + navigator.vibrate(config.VIBRATION_TIME); } if ($el.getAttribute("open-doc") === "true") return; @@ -804,7 +804,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { case "rename": { let newname = await prompt(strings.rename, name, "text", { - match: constants.FILE_NAME_REGEX, + match: config.FILE_NAME_REGEX, }); newname = helpers.fixFilename(newname); @@ -1250,7 +1250,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { } let entryName = await prompt(title, "", "filename", { - match: constants.FILE_NAME_REGEX, + match: config.FILE_NAME_REGEX, required: true, }); @@ -1279,7 +1279,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { loader.destroy(); projectName = await prompt(strings["project name"], project, "text", { required: true, - match: constants.FILE_NAME_REGEX, + match: config.FILE_NAME_REGEX, }); if (!projectName) return; diff --git a/src/pages/plugin/plugin.js b/src/pages/plugin/plugin.js index 56f7f7f86..88fc2f688 100644 --- a/src/pages/plugin/plugin.js +++ b/src/pages/plugin/plugin.js @@ -1,12 +1,11 @@ import "./plugin.scss"; import fsOperation from "fileSystem"; -import ajax from "@deadlyjack/ajax"; import Page from "components/page"; import alert from "dialogs/alert"; import loader from "dialogs/loader"; import purchaseListener from "handlers/purchase"; import actionStack from "lib/actionStack"; -import constants from "lib/constants"; +import config from "lib/config"; import installPlugin from "lib/installPlugin"; import InstallState from "lib/installState"; import settings from "lib/settings"; @@ -55,6 +54,7 @@ export default async function PluginInclude( let purchaseToken; let $settingsIcon; let minVersionCode = -1; + let isSupported = true; actionStack.push({ id: "plugin", @@ -138,9 +138,9 @@ export default async function PluginInclude( await (async () => { try { loader.showTitleLoader(); - if ((await helpers.checkAPIStatus()) && isValidSource(plugin.source)) { + if (isValidSource(plugin.source)) { const remotePlugin = await fsOperation( - constants.API_BASE, + config.API_BASE, `plugin/${id}`, ) .readFile("json") @@ -162,18 +162,26 @@ export default async function PluginInclude( if (!Number.parseFloat(remotePlugin.price)) return; isPaid = remotePlugin.price > 0; - try { - [product] = await helpers.promisify(iap.getProducts, [ - remotePlugin.sku, - ]); - if (product) { - const purchase = await getPurchase(product.productId); - purchased = !!purchase; - price = product.price; - purchaseToken = purchase?.purchaseToken; + purchased = remotePlugin.owned; + price = `₹ ${remotePlugin.price}`; + isSupported = ["all", config.SUPPORTED_EDITOR].includes( + remotePlugin.supported_editor, + ); + + if (!purchased && (await helpers.checkAPIStatus())) { + try { + [product] = await helpers.promisify(iap.getProducts, [ + remotePlugin.sku, + ]); + if (product) { + const purchase = await getPurchase(product.productId); + purchased = !!purchase; + price = product.price; + purchaseToken = purchase?.purchaseToken; + } + } catch (error) { + helpers.error(error); } - } catch (error) { - helpers.error(error); } } } catch (error) { @@ -258,12 +266,13 @@ export default async function PluginInclude( async function onpurchase(e) { const purchase = await getPurchase(product.productId); - await ajax.post(Url.join(constants.API_BASE, "plugin/order"), { - data: { + await fetch(Url.join(config.API_BASE, "plugin/order"), { + method: "POST", + body: JSON.stringify({ id: plugin.id, token: purchase?.purchaseToken, package: BuildInfo.packageName, - }, + }), }); purchaseToken = purchase?.purchaseToken; purchased = !!purchase; @@ -289,16 +298,20 @@ export default async function PluginInclude( try { if (!product) throw new Error("Product not found"); $button.textContent = strings["loading..."]; - const { refer, refunded, error } = await ajax.post( - Url.join(constants.API_BASE, "plugin/refund"), - { - data: { - id: plugin.id, - package: BuildInfo.packageName, - token: purchaseToken, - }, - }, - ); + const res = await fetch(Url.join(config.API_BASE, "plugin/refund"), { + method: "POST", + body: JSON.stringify({ + id: plugin.id, + package: BuildInfo.packageName, + token: purchaseToken, + }), + }); + + if (!res.ok) { + throw new Error("Failed to fetch refund"); + } + + const { refer, refunded, error } = await res.json(); if (refer) { system.openInBrowser(refer); return; @@ -364,6 +377,7 @@ export default async function PluginInclude( uninstall, currentVersion, minVersionCode, + isSupported, }); // Handle anchor links @@ -495,7 +509,5 @@ export default async function PluginInclude( } function isValidSource(source) { - return source - ? source.startsWith(Url.join(constants.API_BASE, "plugin")) - : true; + return source ? source.startsWith(Url.join(config.API_BASE, "plugin")) : true; } diff --git a/src/pages/plugin/plugin.view.js b/src/pages/plugin/plugin.view.js index 3ae08a9c4..503537643 100644 --- a/src/pages/plugin/plugin.view.js +++ b/src/pages/plugin/plugin.view.js @@ -9,7 +9,7 @@ import alert from "dialogs/alert"; import DOMPurify from "dompurify"; import Ref from "html-tag-js/ref"; import actionStack from "lib/actionStack"; -import constants from "lib/constants"; +import config from "lib/config"; import helpers from "utils/helpers"; import Url from "utils/Url"; @@ -53,6 +53,7 @@ export default (props) => { license, changelogs, repository, + isSupported = true, keywords: keywordsRaw, contributors: contributorsRaw, votes_up: votesUp, @@ -209,7 +210,7 @@ export default (props) => { : [{ name: author, role: "Developer", github: authorGithub }]; return contributorsList.map(({ name, role, github }) => { - let dp = Url.join(constants.API_BASE, `../user.png`); + let dp = Url.join(config.API_BASE, `../user.png`); if (github) { dp = `https://avatars.githubusercontent.com/${github}`; } @@ -280,7 +281,23 @@ function Buttons({ price, buy, minVersionCode, + isSupported = true, }) { + if (!isSupported) { + return ( +
+

+ + {strings["plugin-not-supported"]} +

+ {strings["plugin-not-supported-info"]} +
+ ); + } + if ( typeof minVersionCode === "number" && minVersionCode > BuildInfo.versionCode @@ -288,7 +305,7 @@ function Buttons({ return (
- + {strings["plugin min version"] .replace("{name}", name) .replace("{v-code}", minVersionCode)} @@ -412,7 +429,7 @@ async function showReviews(pluginId, author) {
Review @@ -424,7 +441,7 @@ async function showReviews(pluginId, author) { try { const reviews = await fsOperation( - constants.API_BASE, + config.API_BASE, `/comments/${pluginId}`, ).readFile("json"); if (!reviews.length) { @@ -499,7 +516,7 @@ function Review({ author, author_reply: authorReply, }) { - let dp = Url.join(constants.API_BASE, `../user.png`); + let dp = Url.join(config.API_BASE, `../user.png`); let voteImage = Ref(); let review = Ref(); @@ -508,9 +525,9 @@ function Review({ } if (vote === 1) { - voteImage.style.backgroundImage = `url(${Url.join(constants.API_BASE, `../thumbs-up.gif`)})`; + voteImage.style.backgroundImage = `url(${Url.join(config.API_BASE, `../thumbs-up.gif`)})`; } else if (vote === -1) { - voteImage.style.backgroundImage = `url(${Url.join(constants.API_BASE, `../thumbs-down.gif`)})`; + voteImage.style.backgroundImage = `url(${Url.join(config.API_BASE, `../thumbs-down.gif`)})`; } if (authorReply) { diff --git a/src/pages/plugins/index.js b/src/pages/plugins/index.js index e208ae581..28808dfa6 100644 --- a/src/pages/plugins/index.js +++ b/src/pages/plugins/index.js @@ -1,10 +1,8 @@ function plugins(updates) { - import(/* webpackChunkName: "plugins" */ './plugins').then( - (res) => { - const Plugins = res.default; - Plugins(updates); - }, - ); + import(/* webpackChunkName: "plugins" */ "./plugins").then((res) => { + const Plugins = res.default; + Plugins(updates); + }); } export default plugins; diff --git a/src/pages/plugins/item.js b/src/pages/plugins/item.js index f9198d986..c5c2e5c50 100644 --- a/src/pages/plugins/item.js +++ b/src/pages/plugins/item.js @@ -1,5 +1,5 @@ import helpers from "utils/helpers"; -import pluginIcon from './plugin-icon.png'; +import pluginIcon from "./plugin-icon.png"; /** * Creates a plugin list item @@ -15,110 +15,104 @@ import pluginIcon from './plugin-icon.png'; * @returns */ export default function Item({ - id, - name, - icon, - version, - license, - author, - price, - author_verified, - downloads, - installed, - enabled, - onToggleEnabled, - updates, + id, + name, + icon, + version, + license, + author, + price, + author_verified, + downloads, + installed, + enabled, + onToggleEnabled, + updates, }) { - const authorName = (() => { - const displayName = - typeof author === "object" ? author.name : author || "Unknown"; - // Check if it's likely an email or too long - if (displayName.includes("@") || displayName.length > 20) { - return displayName.substring(0, 20) + "..."; - } - return displayName; - })(); + const authorName = (() => { + const displayName = + typeof author === "object" ? author.name : author || "Unknown"; + // Check if it's likely an email or too long + if (displayName.includes("@") || displayName.length > 20) { + return displayName.substring(0, 20) + "..."; + } + return displayName; + })(); - return ( -
-
-
- {name -
-
-
-
- - {name} - - v{version} -
-
-
- - {authorName} - {author_verified ? ( - - ) : ( - "" - )} -
- -
- - {license || "Unknown"} -
- {downloads && ( - <> - -
- - {helpers.formatDownloadCount(downloads)} -
- - )} -
-
- {price !== null && price !== undefined && price !== 0 ? ( - ₹{price} - ) : null} - {installed && !updates ? ( - { - e.stopPropagation(); - onToggleEnabled?.(id, enabled); - }} - > - - - - - ) : null} -
-
-
- ); + return ( +
+
+
+ {name +
+
+
+
+ + {name} + + v{version} +
+
+
+ + {authorName} + {author_verified ? ( + + ) : ( + "" + )} +
+ +
+ + {license || "Unknown"} +
+ {downloads && ( + <> + +
+ + {helpers.formatDownloadCount(downloads)} +
+ + )} +
+
+ {price !== null && price !== undefined && price !== 0 ? ( + ₹{price} + ) : null} + {installed && !updates ? ( + { + e.stopPropagation(); + onToggleEnabled?.(id, enabled); + }} + > + + + + + ) : null} +
+
+
+ ); } diff --git a/src/pages/plugins/plugins.js b/src/pages/plugins/plugins.js index d16aa2dcc..5439ed748 100644 --- a/src/pages/plugins/plugins.js +++ b/src/pages/plugins/plugins.js @@ -1,781 +1,825 @@ import "./plugins.scss"; -import Item from "./item"; -import Url from "utils/Url"; -import Plugin from "pages/plugin"; -import Page from "components/page"; -import helpers from "utils/helpers"; import fsOperation from "fileSystem"; -import constants from "lib/constants"; -import TabView from "components/tabView"; +import Contextmenu from "components/contextmenu"; +import Page from "components/page"; import searchBar from "components/searchbar"; -import FileBrowser from "pages/fileBrowser"; -import installPlugin from "lib/installPlugin"; +import TabView from "components/tabView"; import prompt from "dialogs/prompt"; import actionStack from "lib/actionStack"; -import Contextmenu from "components/contextmenu"; -import settings from "lib/settings"; +import config from "lib/config"; +import installPlugin from "lib/installPlugin"; import loadPlugin from "lib/loadPlugin"; +import settings from "lib/settings"; import { hideAd } from "lib/startAd"; +import FileBrowser from "pages/fileBrowser"; +import Plugin from "pages/plugin"; +import helpers from "utils/helpers"; +import Url from "utils/Url"; +import Item from "./item"; /** * * @param {Array} updates */ export default function PluginsInclude(updates) { - const $page = Page(strings["plugins"]); - const $search = ; - const $add = ; - const $filter = ; - const List = () => ( -
- ); - const $list = { - all: , - installed: , - owned: , - }; - const plugins = { - all: [], - installed: [], - owned: [], - }; - let $currList = $list.installed; - let currSection = "installed"; - let hideSearchBar = () => {}; - let currentPage = 1; - let isLoading = false; - let hasMore = true; - let isSearching = false; - let currentFilter = null; - const LIMIT = 50; - const SUPPORTED_EDITOR = "cm"; - - const withSupportedEditor = (url) => { - const separator = url.includes("?") ? "&" : "?"; - return `${url}${separator}supported_editor=${SUPPORTED_EDITOR}`; - }; - - Contextmenu({ - toggler: $add, - top: "8px", - right: "8px", - items: [ - [strings.remote, "remote"], - [strings.local, "local"], - ], - onselect(item) { - addSource(item); - }, - }); - - const verifiedLabel = strings["verified publisher"]; - const authorLabel = strings.author || strings.name; - const keywordsLabel = strings.keywords; - - const filterOptions = { - "orderBy:top_rated": { type: "orderBy", value: "top_rated", baseLabel: strings.top_rated }, - "orderBy:newest": { type: "orderBy", value: "newest", baseLabel: strings.newly_added }, - "orderBy:downloads": { type: "orderBy", value: "downloads", baseLabel: strings.most_downloaded }, - "attribute:verified": { type: "verified", value: true, baseLabel: verifiedLabel }, - "attribute:author": { type: "author", baseLabel: authorLabel }, - "attribute:keywords": { type: "keywords", baseLabel: keywordsLabel }, - }; - - async function applyFilter(filterState) { - if (!filterState) return; - - const normalizedFilter = { - ...filterState, - displayLabel: filterState.displayLabel || filterState.baseLabel, - nextPage: 1, - buffer: [], - hasMoreSource: true, - }; - - currentFilter = normalizedFilter; - currentPage = 1; - hasMore = true; - isLoading = false; - plugins.all = []; - - if (currSection !== "all") { - render("all"); - } else { - $list.all.replaceChildren(); - } - - const filterMessage = ( -
- {strings["filtered by"]} {normalizedFilter.displayLabel} - -
- ); - - $list.all.append(filterMessage); - $list.all.setAttribute("empty-msg", strings["loading..."]); - await getFilteredPlugins(currentFilter, true); - } - - function clearFilter() { - currentFilter = null; - currentPage = 1; - hasMore = true; - isLoading = false; - plugins.all = []; - $list.all.replaceChildren(); - $list.all.setAttribute("empty-msg", strings["loading..."]); - getAllPlugins(); - } - - Contextmenu({ - toggler: $filter, - top: "8px", - right: "16px", - items: [ - [strings.top_rated, "orderBy:top_rated"], - [strings.newly_added, "orderBy:newest"], - [strings.most_downloaded, "orderBy:downloads"], - [verifiedLabel, "attribute:verified"], - [authorLabel, "attribute:author"], - [keywordsLabel, "attribute:keywords"], - ], - async onselect(action) { - const option = filterOptions[action]; - if (!option) return; - - const filterState = { - type: option.type, - value: option.value, - baseLabel: option.baseLabel, - displayLabel: option.baseLabel, - }; - - if (option.type === "author") { - const authorName = (await prompt("Enter author name", "", "text"))?.trim(); - if (!authorName) return; - filterState.value = authorName.toLowerCase(); - filterState.originalValue = authorName; - filterState.displayLabel = `${option.baseLabel}: ${authorName}`; - } else if (option.type === "keywords") { - const rawKeywords = (await prompt("Enter keywords", "", "text"))?.trim(); - if (!rawKeywords) return; - const keywordList = rawKeywords - .split(",") - .map((item) => item.trim()) - .filter(Boolean); - if (!keywordList.length) return; - filterState.value = keywordList.map((item) => item.toLowerCase()); - filterState.originalValue = keywordList.join(", "); - filterState.displayLabel = `${option.baseLabel}: ${filterState.originalValue}`; - } - - await applyFilter(filterState); - }, - }); - - $page.body = ( - -
- - {strings.installed} - - - {strings.all} - - - {strings.owned} - -
- {$list.installed} -
- ); - $page.header.append($search, $filter, $add); - - actionStack.push({ - id: "plugins", - action: $page.hide, - }); - - $page.onhide = function () { - hideAd(); - actionStack.remove("plugins"); - }; - - $page.onconnect = () => { - $currList.scrollTop = $currList._scroll || 0; - }; - - $page.onwilldisconnect = () => { - $currList._scroll = $currList.scrollTop; - }; - - $page.ondisconnect = () => hideSearchBar(); - - $page.onclick = handleClick; - - $list.all.addEventListener('scroll', async (e) => { - if (isLoading || !hasMore || isSearching) return; - - const { scrollTop, scrollHeight, clientHeight } = e.target; - if (scrollTop + clientHeight >= scrollHeight - 50) { - if (currentFilter) { - await getFilteredPlugins(currentFilter); - } else { - await getAllPlugins(); - } - } - }) - - app.append($page); - helpers.showAd(); - - if (updates) { - $page.get(".options").style.display = "none"; - $add.style.display = "none"; - $filter.style.display = "none"; - $page.settitle(strings.update); - getInstalledPlugins(updates).then(() => { - render("installed"); - }); - return; - } - - if (navigator.onLine) { - getAllPlugins(); - getOwned(); - } - - getInstalledPlugins().then(() => { - if (plugins.installed.length) { - return; - } - - render("all"); - }); - - function handleClick(event) { - const $target = event.target; - const { action } = $target.dataset; - if (action === "search") { - if (currSection === "all") { - isSearching = true; - searchBar( - $currList, - (hide) => { - hideSearchBar = hide; - isSearching = false; - }, - undefined, - searchRemotely, - ); - return; - } else { - isSearching = true; - searchBar($currList, (hide) => { - hideSearchBar = hide; - isSearching = false; - }); - return; - } - } - if (action === "open") { - Plugin($target.dataset, onInstall, onUninstall); - return; - } - } - - function render(section) { - if (currSection === section) return; - - if (!section) { - section = currSection; - } - - if (document.getElementById("search-bar")) { - hideSearchBar(); - } - - const $section = $list[section]; - $currList._scroll = $currList.scrollTop; - $currList.replaceWith($section); - $section.scrollTop = $section._scroll || 0; - $currList = $section; - currSection = section; - if (section === "all") { - currentPage = 1; - hasMore = true; - isLoading = false; - plugins.all = []; // Reset the all plugins array - $list.all.replaceChildren(); - if (!currentFilter) { - getAllPlugins(); - } - } - $page.get(".options .active").classList.remove("active"); - $page.get(`#${section}_plugins`).classList.add("active"); - } - - function renderAll() { - render("all"); - if (currentFilter) { - applyFilter(currentFilter); - } - } - - function renderInstalled() { - render("installed"); - } - - function renderOwned() { - render("owned"); - } - - async function searchRemotely(query) { - if (!query) return []; - try { - const response = await fetch( - withSupportedEditor(`${constants.API_BASE}/plugins?name=${query}`), - ); - const plugins = await response.json(); - // Map the plugins to Item elements and return - return plugins.map((plugin) => ); - } catch (error) { - $list.all.setAttribute("empty-msg", strings["error"]); - window.log("error", "Failed to search remotely:"); - window.log("error", error); - return []; - } - } - - async function getFilteredPlugins(filterState, isInitial = false) { - if (!filterState) return; - if (isLoading || !hasMore) return; - - try { - isLoading = true; - $list.all.setAttribute("empty-msg", strings["loading..."]); - - const { items, hasMore: hasMoreResults } = await retrieveFilteredPlugins(filterState); - - if (currentFilter !== filterState) { - return; - } - - const installed = await fsOperation(PLUGIN_DIR).lsDir(); - const disabledMap = settings.value.pluginsDisabled || {}; - - installed.forEach(({ url }) => { - const plugin = items.find(({ id }) => id === Url.basename(url)); - if (plugin) { - plugin.installed = true; - plugin.enabled = disabledMap[plugin.id] !== true; - plugin.onToggleEnabled = onToggleEnabled; - plugin.localPlugin = getLocalRes(plugin.id, "plugin.json"); - } - }); - - if (isInitial) { - $list.all.querySelectorAll(".filter-empty").forEach((el) => el.remove()); - } - - plugins.all.push(...items); - - const fragment = document.createDocumentFragment(); - items.forEach((plugin) => { - fragment.append(); - }); - - if (fragment.childNodes.length) { - $list.all.append(fragment); - } else if (isInitial) { - $list.all.append( -
{strings["no plugins found"] || "No plugins found"}
, - ); - } - - hasMore = hasMoreResults; - if (!hasMore) { - $list.all.setAttribute("empty-msg", strings["no plugins found"]); - } - } catch (error) { - $list.all.setAttribute("empty-msg", strings["error"]); - console.error("Failed to filter plugins:", error); - hasMore = false; - } finally { - isLoading = false; - } - } - - async function retrieveFilteredPlugins(filterState) { - if (!filterState) return { items: [], hasMore: false }; - - if (filterState.type === "orderBy") { - const page = filterState.nextPage || 1; - try { - let response; - if (filterState.value === "top_rated") { - response = await fetch( - withSupportedEditor( - `${constants.API_BASE}/plugins?explore=random&page=${page}&limit=${LIMIT}`, - ), - ); - } else { - response = await fetch( - withSupportedEditor( - `${constants.API_BASE}/plugin?orderBy=${filterState.value}&page=${page}&limit=${LIMIT}`, - ), - ); - } - const items = await response.json(); - if (!Array.isArray(items)) { - return { items: [], hasMore: false }; - } - filterState.nextPage = page + 1; - const hasMoreResults = items.length === LIMIT; - return { items, hasMore: hasMoreResults }; - } catch (error) { - console.error("Failed to fetch ordered plugins:", error); - return { items: [], hasMore: false }; - } - } - - if (!Array.isArray(filterState.buffer)) { - filterState.buffer = []; - } - if (filterState.hasMoreSource === undefined) { - filterState.hasMoreSource = true; - } - if (!filterState.nextPage) { - filterState.nextPage = 1; - } - - const items = []; - - while (items.length < LIMIT) { - if (filterState.buffer.length) { - items.push(filterState.buffer.shift()); - continue; - } - - if (filterState.hasMoreSource === false) break; - - try { - const page = filterState.nextPage; - const response = await fetch( - withSupportedEditor(`${constants.API_BASE}/plugins?page=${page}&limit=${LIMIT}`), - ); - const data = await response.json(); - filterState.nextPage = page + 1; - - if (!Array.isArray(data) || !data.length) { - filterState.hasMoreSource = false; - break; - } - - if (data.length < LIMIT) { - filterState.hasMoreSource = false; - } - - const matched = data.filter((plugin) => matchesFilter(plugin, filterState)); - filterState.buffer.push(...matched); - } catch (error) { - console.error("Failed to fetch filtered plugins:", error); - filterState.hasMoreSource = false; - break; - } - } - - while (items.length < LIMIT && filterState.buffer.length) { - items.push(filterState.buffer.shift()); - } - - const hasMoreResults = - (filterState.hasMoreSource !== false && filterState.nextPage) || - filterState.buffer.length > 0; - - return { items, hasMore: Boolean(hasMoreResults) }; - } - - function matchesFilter(plugin, filterState) { - if (!plugin) return false; - - switch (filterState.type) { - case "verified": - return Boolean(plugin.author_verified); - case "author": { - const authorName = normalizePluginAuthor(plugin); - if (!authorName) return false; - return authorName.toLowerCase().includes(filterState.value); - } - case "keywords": { - const pluginKeywords = normalizePluginKeywords(plugin) - .map((keyword) => keyword.toLowerCase()) - .filter(Boolean); - if (!pluginKeywords.length) return false; - return filterState.value.some((keyword) => - pluginKeywords.some((pluginKeyword) => pluginKeyword.includes(keyword)), - ); - } - default: - return true; - } - } - - function normalizePluginAuthor(plugin) { - const { author } = plugin || {}; - if (!author) return ""; - if (typeof author === "string") return author; - if (typeof author === "object") { - return author.name || author.username || author.github || ""; - } - return ""; - } - - function normalizePluginKeywords(plugin) { - const { keywords } = plugin || {}; - if (!keywords) return []; - if (Array.isArray(keywords)) return keywords; - if (typeof keywords === "string") { - try { - const parsed = JSON.parse(keywords); - if (Array.isArray(parsed)) return parsed; - } catch (error) { - return keywords - .split(",") - .map((item) => item.trim()) - .filter(Boolean); - } - } - return []; - } - - async function getAllPlugins() { - if (currentFilter) return; - if (isLoading || !hasMore) return; - - try { - isLoading = true; - - $list.all.setAttribute("empty-msg", strings["loading..."]); - - const response = await fetch( - withSupportedEditor(`${constants.API_BASE}/plugins?page=${currentPage}&limit=${LIMIT}`), - ); - const newPlugins = await response.json(); - - if (newPlugins.length < LIMIT) { - hasMore = false; - } - - const installed = await fsOperation(PLUGIN_DIR).lsDir(); - const disabledMap = settings.value.pluginsDisabled || {}; - - installed.forEach(({ url }) => { - const plugin = newPlugins.find(({ id }) => id === Url.basename(url)); - if (plugin) { - plugin.installed = true; - plugin.enabled = disabledMap[plugin.id] !== true; - plugin.onToggleEnabled = onToggleEnabled; - plugin.localPlugin = getLocalRes(plugin.id, "plugin.json"); - } - }); - - // Add plugins to the all plugins array - plugins.all.push(...newPlugins); - - const fragment = document.createDocumentFragment(); - newPlugins.forEach((plugin) => { - fragment.append(); - }); - $list.all.append(fragment); - - currentPage++; - $list.all.setAttribute("empty-msg", strings["no plugins found"]); - } catch (error) { - window.log("error", error); - } finally { - isLoading = false; - } - } - - async function getInstalledPlugins(updates) { - $list.installed.setAttribute("empty-msg", strings["loading..."]); - plugins.installed = []; - const disabledMap = settings.value.pluginsDisabled || {}; - const installed = await fsOperation(PLUGIN_DIR).lsDir(); - await Promise.all( - installed.map(async (item) => { - const id = Url.basename(item.url); - if (!((updates && updates.includes(id)) || !updates)) return; - const url = Url.join(item.url, "plugin.json"); - const plugin = await fsOperation(url).readFile("json"); - const iconUrl = getLocalRes(id, plugin.icon); - plugin.icon = await helpers.toInternalUri(iconUrl); - plugin.installed = true; - plugin.enabled = disabledMap[id] !== true; // default to true - plugin.onToggleEnabled = onToggleEnabled; - plugins.installed.push(plugin); - if ($list.installed.get(`[data-id=\"${id}\"]`)) return; - $list.installed.append(); - }), - ); - $list.installed.setAttribute("empty-msg", strings["no plugins found"]); - } - - async function getOwned() { - $list.owned.setAttribute("empty-msg", strings["loading..."]); - const purchases = await helpers.promisify(iap.getPurchases); - const disabledMap = settings.value.pluginsDisabled || {}; - - purchases.forEach(async ({ productIds }) => { - const [sku] = productIds; - const url = Url.join(constants.API_BASE, "plugin/owned", sku); - const plugin = await fsOperation(url).readFile("json"); - const isInstalled = plugins.installed.find(({ id }) => id === plugin.id); - plugin.installed = !!isInstalled; - - if (plugin.installed) { - plugin.enabled = disabledMap[plugin.id] !== true; - plugin.onToggleEnabled = onToggleEnabled; - } - - plugins.owned.push(plugin); - $list.owned.append(); - }); - $list.owned.setAttribute("empty-msg", strings["no plugins found"]); - } - - function onInstall(plugin) { - if (updates) return; - - if (!plugin || !plugin.id) { - console.error("Invalid plugin object passed to onInstall"); - return; - } - plugin.installed = true; - - const existingIndex = plugins.installed.findIndex(p => p.id === plugin.id); - if (existingIndex === -1) { - plugins.installed.push(plugin); - } else { - // Update existing plugin - plugins.installed[existingIndex] = plugin; - } - - const allPluginIndex = plugins.all.findIndex(p => p.id === plugin.id); - if (allPluginIndex !== -1) { - plugins.all[allPluginIndex] = plugin; - } - - const existingItem = $list.installed.get(`[data-id="${plugin.id}"]`); - if (!existingItem) { - $list.installed.append(); - } - - } - - function onUninstall(pluginId) { - if (!updates) { - plugins.installed = plugins.installed.filter( - (plugin) => plugin.id !== pluginId, - ); - - const plugin = plugins.all.find((plugin) => plugin.id === pluginId); - if (plugin) { - plugin.installed = false; - plugin.localPlugin = null; - } - } - - // Remove from DOM - const existingItem = $list.installed.get(`[data-id="${pluginId}"]`); - if (existingItem) { - existingItem.remove(); - } - } - - function getLocalRes(id, name) { - return Url.join(PLUGIN_DIR, id, name); - } - - async function addSource(sourceType, value = "https://") { - let source; - if (sourceType === "remote") { - source = await prompt("Enter plugin source", value, "url"); - } else { - source = (await FileBrowser("file", "Select plugin source")).url; - } - - if (!source) return; - - try { - await installPlugin(source); - await getInstalledPlugins(); - } catch (error) { - console.error(error); - window.toast(helpers.errorMessage(error)); - addSource(sourceType, source); - } - } - - async function onToggleEnabled(id, enabled) { - const disabledMap = settings.value.pluginsDisabled || {}; - - if (enabled) { - disabledMap[id] = true; - settings.update({ pluginsDisabled: disabledMap }, false); - window.acode.unmountPlugin(id); - window.toast(strings["plugin_disabled"] || "Plugin Disabled"); - } else { - delete disabledMap[id]; - settings.update({ pluginsDisabled: disabledMap }, false); - await loadPlugin(id); - window.toast(strings["plugin_enabled"] || "Plugin enabled"); - } - - // Update the plugin object's state in all plugin arrays - const installedPlugin = plugins.installed.find(p => p.id === id); - if (installedPlugin) { - installedPlugin.enabled = !enabled; - } - - const allPlugin = plugins.all.find(p => p.id === id); - if (allPlugin) { - allPlugin.enabled = !enabled; - } - - const ownedPlugin = plugins.owned.find(p => p.id === id); - if (ownedPlugin) { - ownedPlugin.enabled = !enabled; - } - - // Re-render the specific item in all tabs - const $installedItem = $list.installed.get(`[data-id="${id}"]`); - if ($installedItem && installedPlugin) { - const $newItem = ; - $installedItem.replaceWith($newItem); - } - - const $allItem = $list.all.get(`[data-id="${id}"]`); - if ($allItem && allPlugin) { - const $newItem = ; - $allItem.replaceWith($newItem); - } - - const $ownedItem = $list.owned.get(`[data-id="${id}"]`); - if ($ownedItem && ownedPlugin) { - const $newItem = ; - $ownedItem.replaceWith($newItem); - } - } + const $page = Page(strings["plugins"]); + const $search = ; + const $add = ; + const $filter = ; + const List = () => ( +
+ ); + const $list = { + all: , + installed: , + owned: , + }; + const plugins = { + all: [], + installed: [], + owned: [], + }; + let $currList = $list.installed; + let currSection = "installed"; + let hideSearchBar = () => {}; + let currentPage = 1; + let isLoading = false; + let hasMore = true; + let isSearching = false; + let currentFilter = null; + const LIMIT = 50; + const SUPPORTED_EDITOR = "cm"; + + Contextmenu({ + toggler: $add, + top: "8px", + right: "8px", + items: [ + [strings.remote, "remote"], + [strings.local, "local"], + ], + onselect(item) { + addSource(item); + }, + }); + + const verifiedLabel = strings["verified publisher"]; + const authorLabel = strings.author || strings.name; + const keywordsLabel = strings.keywords; + + const filterOptions = { + "orderBy:top_rated": { + type: "orderBy", + value: "top_rated", + baseLabel: strings.top_rated, + }, + "orderBy:newest": { + type: "orderBy", + value: "newest", + baseLabel: strings.newly_added, + }, + "orderBy:downloads": { + type: "orderBy", + value: "downloads", + baseLabel: strings.most_downloaded, + }, + "attribute:verified": { + type: "verified", + value: true, + baseLabel: verifiedLabel, + }, + "attribute:author": { type: "author", baseLabel: authorLabel }, + "attribute:keywords": { type: "keywords", baseLabel: keywordsLabel }, + }; + + async function applyFilter(filterState) { + if (!filterState) return; + + const normalizedFilter = { + ...filterState, + displayLabel: filterState.displayLabel || filterState.baseLabel, + nextPage: 1, + buffer: [], + hasMoreSource: true, + }; + + currentFilter = normalizedFilter; + currentPage = 1; + hasMore = true; + isLoading = false; + plugins.all = []; + + if (currSection !== "all") { + render("all"); + } else { + $list.all.replaceChildren(); + } + + const filterMessage = ( +
+ {strings["filtered by"]}{" "} + {normalizedFilter.displayLabel} + +
+ ); + + $list.all.append(filterMessage); + $list.all.setAttribute("empty-msg", strings["loading..."]); + await getFilteredPlugins(currentFilter, true); + } + + function clearFilter() { + currentFilter = null; + currentPage = 1; + hasMore = true; + isLoading = false; + plugins.all = []; + $list.all.replaceChildren(); + $list.all.setAttribute("empty-msg", strings["loading..."]); + getAllPlugins(); + } + + Contextmenu({ + toggler: $filter, + top: "8px", + right: "16px", + items: [ + [strings.top_rated, "orderBy:top_rated"], + [strings.newly_added, "orderBy:newest"], + [strings.most_downloaded, "orderBy:downloads"], + [verifiedLabel, "attribute:verified"], + [authorLabel, "attribute:author"], + [keywordsLabel, "attribute:keywords"], + ], + async onselect(action) { + const option = filterOptions[action]; + if (!option) return; + + const filterState = { + type: option.type, + value: option.value, + baseLabel: option.baseLabel, + displayLabel: option.baseLabel, + }; + + if (option.type === "author") { + const authorName = ( + await prompt("Enter author name", "", "text") + )?.trim(); + if (!authorName) return; + filterState.value = authorName.toLowerCase(); + filterState.originalValue = authorName; + filterState.displayLabel = `${option.baseLabel}: ${authorName}`; + } else if (option.type === "keywords") { + const rawKeywords = ( + await prompt("Enter keywords", "", "text") + )?.trim(); + if (!rawKeywords) return; + const keywordList = rawKeywords + .split(",") + .map((item) => item.trim()) + .filter(Boolean); + if (!keywordList.length) return; + filterState.value = keywordList.map((item) => item.toLowerCase()); + filterState.originalValue = keywordList.join(", "); + filterState.displayLabel = `${option.baseLabel}: ${filterState.originalValue}`; + } + + await applyFilter(filterState); + }, + }); + + $page.body = ( + +
+ + {strings.installed} + + + {strings.all} + + + {strings.owned} + +
+ {$list.installed} +
+ ); + $page.header.append($search, $filter, $add); + + actionStack.push({ + id: "plugins", + action: $page.hide, + }); + + $page.onhide = function () { + hideAd(); + actionStack.remove("plugins"); + }; + + $page.onconnect = () => { + $currList.scrollTop = $currList._scroll || 0; + }; + + $page.onwilldisconnect = () => { + $currList._scroll = $currList.scrollTop; + }; + + $page.ondisconnect = () => hideSearchBar(); + + $page.onclick = handleClick; + + $list.all.addEventListener("scroll", async (e) => { + if (isLoading || !hasMore || isSearching) return; + + const { scrollTop, scrollHeight, clientHeight } = e.target; + if (scrollTop + clientHeight >= scrollHeight - 50) { + if (currentFilter) { + await getFilteredPlugins(currentFilter); + } else { + await getAllPlugins(); + } + } + }); + + app.append($page); + helpers.showAd(); + + if (updates) { + $page.get(".options").style.display = "none"; + $add.style.display = "none"; + $filter.style.display = "none"; + $page.settitle(strings.update); + getInstalledPlugins(updates).then(() => { + render("installed"); + }); + return; + } + + if (navigator.onLine) { + getAllPlugins(); + getOwned(); + } + + getInstalledPlugins().then(() => { + if (plugins.installed.length) { + return; + } + + render("all"); + }); + + function handleClick(event) { + const $target = event.target; + const { action } = $target.dataset; + if (action === "search") { + if (currSection === "all") { + isSearching = true; + searchBar( + $currList, + (hide) => { + hideSearchBar = hide; + isSearching = false; + }, + undefined, + searchRemotely, + ); + return; + } else { + isSearching = true; + searchBar($currList, (hide) => { + hideSearchBar = hide; + isSearching = false; + }); + return; + } + } + if (action === "open") { + Plugin($target.dataset, onInstall, onUninstall); + return; + } + } + + function render(section) { + if (currSection === section) return; + + if (!section) { + section = currSection; + } + + if (document.getElementById("search-bar")) { + hideSearchBar(); + } + + const $section = $list[section]; + $currList._scroll = $currList.scrollTop; + $currList.replaceWith($section); + $section.scrollTop = $section._scroll || 0; + $currList = $section; + currSection = section; + if (section === "all") { + currentPage = 1; + hasMore = true; + isLoading = false; + plugins.all = []; // Reset the all plugins array + $list.all.replaceChildren(); + if (!currentFilter) { + getAllPlugins(); + } + } + $page.get(".options .active").classList.remove("active"); + $page.get(`#${section}_plugins`).classList.add("active"); + } + + function renderAll() { + render("all"); + if (currentFilter) { + applyFilter(currentFilter); + } + } + + function renderInstalled() { + render("installed"); + } + + function renderOwned() { + render("owned"); + } + + async function searchRemotely(query) { + if (!query) return []; + try { + const response = await fetch(`${config.API_BASE}/plugins?name=${query}`); + const plugins = await response.json(); + // Map the plugins to Item elements and return + return plugins.map((plugin) => ); + } catch (error) { + $list.all.setAttribute("empty-msg", strings["error"]); + window.log("error", "Failed to search remotely:"); + window.log("error", error); + return []; + } + } + + async function getFilteredPlugins(filterState, isInitial = false) { + if (!filterState) return; + if (isLoading || !hasMore) return; + + try { + isLoading = true; + $list.all.setAttribute("empty-msg", strings["loading..."]); + + const { items, hasMore: hasMoreResults } = + await retrieveFilteredPlugins(filterState); + + if (currentFilter !== filterState) { + return; + } + + const installed = await fsOperation(PLUGIN_DIR).lsDir(); + const disabledMap = settings.value.pluginsDisabled || {}; + + installed.forEach(({ url }) => { + const plugin = items.find(({ id }) => id === Url.basename(url)); + if (plugin) { + plugin.installed = true; + plugin.enabled = disabledMap[plugin.id] !== true; + plugin.onToggleEnabled = onToggleEnabled; + plugin.localPlugin = getLocalRes(plugin.id, "plugin.json"); + } + }); + + if (isInitial) { + $list.all + .querySelectorAll(".filter-empty") + .forEach((el) => el.remove()); + } + + plugins.all.push(...items); + + const fragment = document.createDocumentFragment(); + items.forEach((plugin) => { + fragment.append(); + }); + + if (fragment.childNodes.length) { + $list.all.append(fragment); + } else if (isInitial) { + $list.all.append( +
+ {strings["no plugins found"] || "No plugins found"} +
, + ); + } + + hasMore = hasMoreResults; + if (!hasMore) { + $list.all.setAttribute("empty-msg", strings["no plugins found"]); + } + } catch (error) { + $list.all.setAttribute("empty-msg", strings["error"]); + console.error("Failed to filter plugins:", error); + hasMore = false; + } finally { + isLoading = false; + } + } + + async function retrieveFilteredPlugins(filterState) { + if (!filterState) return { items: [], hasMore: false }; + + if (filterState.type === "orderBy") { + const page = filterState.nextPage || 1; + try { + let response; + if (filterState.value === "top_rated") { + response = await fetch( + `${config.API_BASE}/plugins?explore=random&page=${page}&limit=${LIMIT}`, + ); + } else { + response = await fetch( + `${config.API_BASE}/plugin?orderBy=${filterState.value}&page=${page}&limit=${LIMIT}`, + ); + } + const items = await response.json(); + if (!Array.isArray(items)) { + return { items: [], hasMore: false }; + } + filterState.nextPage = page + 1; + const hasMoreResults = items.length === LIMIT; + return { items, hasMore: hasMoreResults }; + } catch (error) { + console.error("Failed to fetch ordered plugins:", error); + return { items: [], hasMore: false }; + } + } + + if (!Array.isArray(filterState.buffer)) { + filterState.buffer = []; + } + if (filterState.hasMoreSource === undefined) { + filterState.hasMoreSource = true; + } + if (!filterState.nextPage) { + filterState.nextPage = 1; + } + + const items = []; + + while (items.length < LIMIT) { + if (filterState.buffer.length) { + items.push(filterState.buffer.shift()); + continue; + } + + if (filterState.hasMoreSource === false) break; + + try { + const page = filterState.nextPage; + const response = await fetch( + `${config.API_BASE}/plugins?page=${page}&limit=${LIMIT}`, + ); + const data = await response.json(); + filterState.nextPage = page + 1; + + if (!Array.isArray(data) || !data.length) { + filterState.hasMoreSource = false; + break; + } + + if (data.length < LIMIT) { + filterState.hasMoreSource = false; + } + + const matched = data.filter((plugin) => + matchesFilter(plugin, filterState), + ); + filterState.buffer.push(...matched); + } catch (error) { + console.error("Failed to fetch filtered plugins:", error); + filterState.hasMoreSource = false; + break; + } + } + + while (items.length < LIMIT && filterState.buffer.length) { + items.push(filterState.buffer.shift()); + } + + const hasMoreResults = + (filterState.hasMoreSource !== false && filterState.nextPage) || + filterState.buffer.length > 0; + + return { items, hasMore: Boolean(hasMoreResults) }; + } + + function matchesFilter(plugin, filterState) { + if (!plugin) return false; + + switch (filterState.type) { + case "verified": + return Boolean(plugin.author_verified); + case "author": { + const authorName = normalizePluginAuthor(plugin); + if (!authorName) return false; + return authorName.toLowerCase().includes(filterState.value); + } + case "keywords": { + const pluginKeywords = normalizePluginKeywords(plugin) + .map((keyword) => keyword.toLowerCase()) + .filter(Boolean); + if (!pluginKeywords.length) return false; + return filterState.value.some((keyword) => + pluginKeywords.some((pluginKeyword) => + pluginKeyword.includes(keyword), + ), + ); + } + default: + return true; + } + } + + function normalizePluginAuthor(plugin) { + const { author } = plugin || {}; + if (!author) return ""; + if (typeof author === "string") return author; + if (typeof author === "object") { + return author.name || author.username || author.github || ""; + } + return ""; + } + + function normalizePluginKeywords(plugin) { + const { keywords } = plugin || {}; + if (!keywords) return []; + if (Array.isArray(keywords)) return keywords; + if (typeof keywords === "string") { + try { + const parsed = JSON.parse(keywords); + if (Array.isArray(parsed)) return parsed; + } catch (error) { + return keywords + .split(",") + .map((item) => item.trim()) + .filter(Boolean); + } + } + return []; + } + + async function getAllPlugins() { + if (currentFilter) return; + if (isLoading || !hasMore) return; + + try { + isLoading = true; + + $list.all.setAttribute("empty-msg", strings["loading..."]); + + const response = await fetch( + `${config.API_BASE}/plugins?page=${currentPage}&limit=${LIMIT}`, + ); + const newPlugins = await response.json(); + + if (newPlugins.length < LIMIT) { + hasMore = false; + } + + const installed = await fsOperation(PLUGIN_DIR).lsDir(); + const disabledMap = settings.value.pluginsDisabled || {}; + + installed.forEach(({ url }) => { + const plugin = newPlugins.find(({ id }) => id === Url.basename(url)); + if (plugin) { + plugin.installed = true; + plugin.enabled = disabledMap[plugin.id] !== true; + plugin.onToggleEnabled = onToggleEnabled; + plugin.localPlugin = getLocalRes(plugin.id, "plugin.json"); + } + }); + + // Add plugins to the all plugins array + plugins.all.push(...newPlugins); + + const fragment = document.createDocumentFragment(); + newPlugins.forEach((plugin) => { + fragment.append(); + }); + $list.all.append(fragment); + + currentPage++; + $list.all.setAttribute("empty-msg", strings["no plugins found"]); + } catch (error) { + window.log("error", error); + } finally { + isLoading = false; + } + } + + async function getInstalledPlugins(updates) { + $list.installed.setAttribute("empty-msg", strings["loading..."]); + plugins.installed = []; + const disabledMap = settings.value.pluginsDisabled || {}; + const installed = await fsOperation(PLUGIN_DIR).lsDir(); + await Promise.all( + installed.map(async (item) => { + const id = Url.basename(item.url); + if (!((updates && updates.includes(id)) || !updates)) return; + const url = Url.join(item.url, "plugin.json"); + const plugin = await fsOperation(url).readFile("json"); + const iconUrl = getLocalRes(id, plugin.icon); + plugin.icon = await helpers.toInternalUri(iconUrl); + plugin.installed = true; + plugin.enabled = disabledMap[id] !== true; // default to true + plugin.onToggleEnabled = onToggleEnabled; + plugins.installed.push(plugin); + if ($list.installed.get(`[data-id=\"${id}\"]`)) return; + $list.installed.append(); + }), + ); + $list.installed.setAttribute("empty-msg", strings["no plugins found"]); + } + + async function getOwned() { + $list.owned.setAttribute("empty-msg", strings["loading..."]); + const purchases = await helpers.promisify(iap.getPurchases); + const disabledMap = settings.value.pluginsDisabled || {}; + + purchases.forEach(async ({ productIds }) => { + const [sku] = productIds; + const url = Url.join(config.API_BASE, "plugin/owned", sku); + const plugin = await fsOperation(url).readFile("json"); + const isInstalled = plugins.installed.find(({ id }) => id === plugin.id); + plugin.installed = !!isInstalled; + + if (plugin.installed) { + plugin.enabled = disabledMap[plugin.id] !== true; + plugin.onToggleEnabled = onToggleEnabled; + } + + plugins.owned.push(plugin); + $list.owned.append(); + }); + + try { + const res = await fetch(`${config.API_BASE}/plugins?owned=true`); + if (res.ok) { + const ownedPlugins = await res.json(); + if (Array.isArray(ownedPlugins)) { + ownedPlugins.forEach((plugin) => { + if ( + !purchases.find(({ productIds }) => + productIds.includes(plugin.sku), + ) + ) { + plugins.owned.push(plugin); + $list.owned.append(); + } + }); + } + } + } catch (error) {} + + $list.owned.setAttribute("empty-msg", strings["no plugins found"]); + } + + function onInstall(plugin) { + if (updates) return; + + if (!plugin || !plugin.id) { + console.error("Invalid plugin object passed to onInstall"); + return; + } + plugin.installed = true; + + const existingIndex = plugins.installed.findIndex( + (p) => p.id === plugin.id, + ); + if (existingIndex === -1) { + plugins.installed.push(plugin); + } else { + // Update existing plugin + plugins.installed[existingIndex] = plugin; + } + + const allPluginIndex = plugins.all.findIndex((p) => p.id === plugin.id); + if (allPluginIndex !== -1) { + plugins.all[allPluginIndex] = plugin; + } + + const existingItem = $list.installed.get(`[data-id="${plugin.id}"]`); + if (!existingItem) { + $list.installed.append(); + } + } + + function onUninstall(pluginId) { + if (!updates) { + plugins.installed = plugins.installed.filter( + (plugin) => plugin.id !== pluginId, + ); + + const plugin = plugins.all.find((plugin) => plugin.id === pluginId); + if (plugin) { + plugin.installed = false; + plugin.localPlugin = null; + } + } + + // Remove from DOM + const existingItem = $list.installed.get(`[data-id="${pluginId}"]`); + if (existingItem) { + existingItem.remove(); + } + } + + function getLocalRes(id, name) { + return Url.join(PLUGIN_DIR, id, name); + } + + async function addSource(sourceType, value = "https://") { + let source; + if (sourceType === "remote") { + source = await prompt("Enter plugin source", value, "url"); + } else { + source = (await FileBrowser("file", "Select plugin source")).url; + } + + if (!source) return; + + try { + await installPlugin(source); + await getInstalledPlugins(); + } catch (error) { + console.error(error); + window.toast(helpers.errorMessage(error)); + addSource(sourceType, source); + } + } + + async function onToggleEnabled(id, enabled) { + const disabledMap = settings.value.pluginsDisabled || {}; + + if (enabled) { + disabledMap[id] = true; + settings.update({ pluginsDisabled: disabledMap }, false); + window.acode.unmountPlugin(id); + window.toast(strings["plugin_disabled"] || "Plugin Disabled"); + } else { + delete disabledMap[id]; + settings.update({ pluginsDisabled: disabledMap }, false); + await loadPlugin(id); + window.toast(strings["plugin_enabled"] || "Plugin enabled"); + } + + // Update the plugin object's state in all plugin arrays + const installedPlugin = plugins.installed.find((p) => p.id === id); + if (installedPlugin) { + installedPlugin.enabled = !enabled; + } + + const allPlugin = plugins.all.find((p) => p.id === id); + if (allPlugin) { + allPlugin.enabled = !enabled; + } + + const ownedPlugin = plugins.owned.find((p) => p.id === id); + if (ownedPlugin) { + ownedPlugin.enabled = !enabled; + } + + // Re-render the specific item in all tabs + const $installedItem = $list.installed.get(`[data-id="${id}"]`); + if ($installedItem && installedPlugin) { + const $newItem = ; + $installedItem.replaceWith($newItem); + } + + const $allItem = $list.all.get(`[data-id="${id}"]`); + if ($allItem && allPlugin) { + const $newItem = ; + $allItem.replaceWith($newItem); + } + + const $ownedItem = $list.owned.get(`[data-id="${id}"]`); + if ($ownedItem && ownedPlugin) { + const $newItem = ; + $ownedItem.replaceWith($newItem); + } + } } diff --git a/src/pages/sponsor/sponsor.js b/src/pages/sponsor/sponsor.js index 9d83c79fb..fe5db6b79 100644 --- a/src/pages/sponsor/sponsor.js +++ b/src/pages/sponsor/sponsor.js @@ -1,6 +1,5 @@ import "./style.scss"; import fsOperation from "fileSystem"; -import ajax from "@deadlyjack/ajax"; import Logo from "components/logo"; import Page from "components/page"; import alert from "dialogs/alert"; @@ -8,7 +7,7 @@ import box from "dialogs/box"; import loader from "dialogs/loader"; import multiPrompt from "dialogs/multiPrompt"; import actionStack from "lib/actionStack"; -import constants from "lib/constants"; +import config from "lib/config"; import helpers from "utils/helpers"; //TODO: fix (-1 means, user is not logged in to any google account) @@ -18,7 +17,6 @@ import helpers from "utils/helpers"; * @param {() => void} onclose */ export default function Sponsor(onclose) { - const BASE_URL = "https://acode.app/res/"; const $page = Page(strings.sponsor); let cancel = false; @@ -68,14 +66,13 @@ export default function Sponsor(onclose) { msg += `Error: ${rejectedPromise.reason}\n`; msg += `Code: ${rejectedPromise.value.resCode}`; } else { - const blob = await ajax({ - url: BASE_URL + "6.jpeg", - responseType: "blob", - }).catch((err) => { - helpers.error(err); - }); - const url = URL.createObjectURL(blob); - msg = ``; + const res = await fetch(`${config.BASE_URL}/res/6.jpeg`); + + if (res.ok) { + const url = URL.createObjectURL(await res.blob()); + msg = ``; + } + msg += "

Thank you for supporting Acode!

"; } @@ -86,13 +83,14 @@ export default function Sponsor(onclose) { ); try { - const res = await ajax.post(`${constants.API_BASE}/sponsor`, { - data: { + const res = await fetch(`${config.API_BASE}/sponsor`, { + method: "POST", + body: JSON.stringify({ ...sponsorDetails, tier: productId, packageName: BuildInfo.packageName, purchaseToken: order.purchaseToken, - }, + }), }); if (res.error) { helpers.error(res.error); @@ -130,7 +128,7 @@ export default function Sponsor(onclose) { async function render() { let products = await new Promise((resolve, reject) => { iap.getProducts( - constants.SKU_LIST, + config.SKU_LIST, (products) => { resolve(products); }, diff --git a/src/pages/sponsors/sponsors.js b/src/pages/sponsors/sponsors.js index a965d8ed0..67f1d9f2c 100644 --- a/src/pages/sponsors/sponsors.js +++ b/src/pages/sponsors/sponsors.js @@ -1,10 +1,9 @@ -import ajax from "@deadlyjack/ajax"; import "./style.scss"; import Page from "components/page"; import toast from "components/toast"; import Ref from "html-tag-js/ref"; import actionStack from "lib/actionStack"; -import constants from "lib/constants"; +import config from "lib/config"; import Sponsor from "pages/sponsor"; import helpers from "utils/helpers"; @@ -113,12 +112,17 @@ export default function Sponsors() { async function render() { let sponsors = []; try { - const res = await ajax.get(`${constants.API_BASE}/sponsors`); - if (res.error) { + const res = await fetch(`${config.API_BASE}/sponsors`); + if (!res.ok) { + throw new Error("Failed to fetch sponsor"); + } + + const sponsorList = await res.json(); + if (sponsorList.error) { toast("Unable to load sponsors..."); - console.error("Error loading sponsors:", res.error); + console.error("Error loading sponsors:", sponsorList.error); } else { - sponsors = res; + sponsors = sponsorList; localStorage.setItem("cached_sponsors", JSON.stringify(sponsors)); } } catch (error) { diff --git a/src/pages/themeSetting/themeSetting.js b/src/pages/themeSetting/themeSetting.js index d78f5419f..86839a47e 100644 --- a/src/pages/themeSetting/themeSetting.js +++ b/src/pages/themeSetting/themeSetting.js @@ -11,6 +11,7 @@ import TabView from "components/tabView"; import alert from "dialogs/alert"; import Ref from "html-tag-js/ref"; import actionStack from "lib/actionStack"; +import config from "lib/config"; import removeAds from "lib/removeAds"; import appSettings from "lib/settings"; import { hideAd } from "lib/startAd"; @@ -115,7 +116,7 @@ export default function () { themes.list().forEach((themeSummary) => { const theme = themes.get(themeSummary.id); const isCurrentTheme = theme.id === currentTheme; - const isPremium = theme.version === "paid" && IS_FREE_VERSION; + const isPremium = theme.version === "paid" && !config.HAS_PRO; const $item = (

CONNECT

- - + + - +
diff --git a/src/palettes/changeTheme/index.js b/src/palettes/changeTheme/index.js index 7827f7054..b76e88f1d 100644 --- a/src/palettes/changeTheme/index.js +++ b/src/palettes/changeTheme/index.js @@ -1,5 +1,6 @@ import "./style.scss"; import palette from "components/palette"; +import config from "lib/config"; import appSettings from "lib/settings"; import { isDeviceDarkTheme } from "lib/systemConfiguration"; import themes from "theme/list"; @@ -22,7 +23,7 @@ function generateHints(type) { const currentTheme = appSettings.value.appTheme; const availableThemes = themes .list() - .filter((theme) => !(theme.version === "paid" && IS_FREE_VERSION)); + .filter((theme) => !(theme.version === "paid" && !config.HAS_PRO)); return availableThemes.map((theme) => { const isCurrent = theme.id === currentTheme; diff --git a/src/plugins/auth/plugin.xml b/src/plugins/auth/plugin.xml index e6e3e448c..521b422a3 100644 --- a/src/plugins/auth/plugin.xml +++ b/src/plugins/auth/plugin.xml @@ -7,6 +7,7 @@ + diff --git a/src/plugins/auth/src/android/Authenticator.java b/src/plugins/auth/src/android/Authenticator.java index 48bf3cc08..9458fec83 100644 --- a/src/plugins/auth/src/android/Authenticator.java +++ b/src/plugins/auth/src/android/Authenticator.java @@ -1,45 +1,52 @@ package com.foxdebug.acode.rk.auth; -import android.util.Log; // Required for logging +import android.util.Log; +import android.webkit.CookieManager; +import android.webkit.WebView; import com.foxdebug.acode.rk.auth.EncryptedPreferenceManager; import org.apache.cordova.*; import org.json.JSONArray; import org.json.JSONException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.Scanner; public class Authenticator extends CordovaPlugin { - // Standard practice: use a TAG for easy filtering in Logcat - private static final String TAG = "AcodeAuth"; + private static final String TAG = "AcodeAuth"; private static final String PREFS_FILENAME = "acode_auth_secure"; private static final String KEY_TOKEN = "auth_token"; + private static final String[] API_ORIGINS = { + "https://acode.app", + "https://dev.acode.app" + }; private EncryptedPreferenceManager prefManager; @Override protected void pluginInitialize() { Log.d(TAG, "Initializing Authenticator Plugin..."); this.prefManager = new EncryptedPreferenceManager(this.cordova.getContext(), PREFS_FILENAME); + + WebView androidWebView = (WebView) webView.getView(); + CookieManager.getInstance().setAcceptThirdPartyCookies(androidWebView, true); + + String token = prefManager.getString(KEY_TOKEN, ""); + if (!token.isEmpty()) { + setTokenCookie(token); + } } @Override public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { Log.i(TAG, "Native Action Called: " + action); - + switch (action) { case "logout": - this.logout(callbackContext); - return true; - case "isLoggedIn": - this.isLoggedIn(callbackContext); - return true; - case "getUserInfo": - this.getUserInfo(callbackContext); + prefManager.remove(KEY_TOKEN); + cordova.getActivity().runOnUiThread(() -> clearTokenCookie()); + if (callbackContext != null) callbackContext.success(); return true; case "saveToken": String token = args.getString(0); Log.d(TAG, "Saving new token..."); prefManager.setString(KEY_TOKEN, token); + cordova.getActivity().runOnUiThread(() -> setTokenCookie(token)); callbackContext.success(); return true; default: @@ -48,101 +55,19 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo } } - private void logout(CallbackContext callbackContext) { - Log.d(TAG, "Logging out, removing token."); - prefManager.remove(KEY_TOKEN); - if (callbackContext != null) callbackContext.success(); - } - - private void isLoggedIn(CallbackContext callbackContext) { - String token = prefManager.getString(KEY_TOKEN, null); - if (token == null) { - Log.d(TAG, "isLoggedIn check: No token found in preferences."); - callbackContext.error(0); - return; - } - - Log.d(TAG, "isLoggedIn check: Token found, validating with server..."); - final String tokenToValidate = token; // Make effectively final for lambda - - cordova.getThreadPool().execute(() -> { - try { - String result = validateToken(tokenToValidate); - if (result != null) { - Log.i(TAG, "Token validation successful."); - callbackContext.success(); - } else { - Log.w(TAG, "Token validation failed (result was null)."); - callbackContext.error(401); - } - } catch (Exception e) { - Log.e(TAG, "CRITICAL error in isLoggedIn thread: " + e.getMessage(), e); - callbackContext.error("Internal Plugin Error: " + e.getMessage()); - } - }); - } - - private void getUserInfo(CallbackContext callbackContext) { - Log.d(TAG, "getUserInfo: Fetching token..."); - String token = prefManager.getString(KEY_TOKEN, null); - - if (token == null) { - Log.e(TAG, "getUserInfo: No token found."); - callbackContext.error(0); - return; + private void setTokenCookie(String token) { + CookieManager cm = CookieManager.getInstance(); + for (String origin : API_ORIGINS) { + cm.setCookie(origin, "token=" + token + "; Path=/; Secure; HttpOnly; SameSite=None"); } - - final String tokenToValidate = token; - cordova.getThreadPool().execute(() -> { - try { - String response = validateToken(tokenToValidate); - if (response != null) { - Log.d(TAG, "getUserInfo: Successfully fetched user info."); - callbackContext.success(response); - } else { - Log.e(TAG, "getUserInfo: Validation failed or unauthorized."); - callbackContext.error("Unauthorized"); - } - } catch (Exception e) { - Log.e(TAG, "Error in getUserInfo: " + e.getMessage(), e); - callbackContext.error("Error: " + e.getMessage()); - } - }); + cm.flush(); } - private String validateToken(String token) { - HttpURLConnection conn = null; - try { - Log.d(TAG, "Network Request: Connecting to https://acode.app/api/login"); - URL url = new URL("https://acode.app/api/login"); // Changed from /api to /api/login - conn = (HttpURLConnection) url.openConnection(); - conn.setRequestProperty("x-auth-token", token); - conn.setRequestMethod("GET"); - conn.setConnectTimeout(5000); - conn.setReadTimeout(5000); // Add read timeout too - - int code = conn.getResponseCode(); - Log.d(TAG, "Server responded with status code: " + code); - - if (code == 200) { - Scanner s = new Scanner(conn.getInputStream(), "UTF-8").useDelimiter("\\A"); - String response = s.hasNext() ? s.next() : ""; - Log.d(TAG, "Response received: " + response); // Debug log - return response; - } else if (code == 401) { - Log.w(TAG, "401 Unauthorized: Logging user out native-side."); - logout(null); - } else { - Log.w(TAG, "Unexpected status code: " + code); - } - } catch (Exception e) { - Log.e(TAG, "Network Exception in validateToken: " + e.getMessage(), e); - e.printStackTrace(); // Print full stack trace for debugging - } finally { - if (conn != null) conn.disconnect(); + private void clearTokenCookie() { + CookieManager cm = CookieManager.getInstance(); + for (String origin : API_ORIGINS) { + cm.setCookie(origin, "token=; Path=/; Max-Age=0"); } - return null; + cm.flush(); } - - } diff --git a/src/settings/appSettings.js b/src/settings/appSettings.js index c2284cbce..bea578723 100644 --- a/src/settings/appSettings.js +++ b/src/settings/appSettings.js @@ -1,12 +1,11 @@ import fsOperation from "fileSystem"; -import ajax from "@deadlyjack/ajax"; import { resetKeyBindings } from "cm/commandRegistry"; import settingsPage from "components/settingsPage"; import loader from "dialogs/loader"; import select from "dialogs/select"; import actions from "handlers/quickTools"; import actionStack from "lib/actionStack"; -import constants from "lib/constants"; +import config from "lib/config"; import fonts from "lib/fonts"; import lang from "lib/lang"; import openFile from "lib/openFile"; @@ -354,11 +353,9 @@ export default function otherSettings() { strings["downloading..."], ); try { - const erudaScript = await ajax({ - url: constants.ERUDA_CDN, - responseType: "text", - contentType: "application/x-www-form-urlencoded", - }); + const erudaScript = await fsOperation(config.ERUDA_CDN).readFile( + "utf-8", + ); await fsOperation(DATA_STORAGE).createFile("eruda.js", erudaScript); loader.destroy(); } catch (error) { diff --git a/src/settings/backupRestore.js b/src/settings/backupRestore.js index e95b2b01b..6eebf73c3 100644 --- a/src/settings/backupRestore.js +++ b/src/settings/backupRestore.js @@ -4,7 +4,7 @@ import toast from "components/toast"; import alert from "dialogs/alert"; import confirm from "dialogs/confirm"; import loader from "dialogs/loader"; -import constants from "lib/constants"; +import config from "lib/config"; import appSettings from "lib/settings"; import FileBrowser from "pages/fileBrowser"; import helpers from "utils/helpers"; @@ -519,7 +519,7 @@ backupRestore.restore = async function (url) { } } else { // Remote plugin case - fetch from API - const pluginUrl = Url.join(constants.API_BASE, `plugin/${id}`); + const pluginUrl = Url.join(config.API_BASE, `plugin/${id}`); let remotePlugin = null; try { diff --git a/src/settings/editorSettings.js b/src/settings/editorSettings.js index 2a466c7b4..cf31d2bb7 100644 --- a/src/settings/editorSettings.js +++ b/src/settings/editorSettings.js @@ -1,5 +1,5 @@ import settingsPage from "components/settingsPage"; -import constants from "lib/constants"; +import config from "lib/config"; import fonts from "lib/fonts"; import appSettings from "lib/settings"; import scrollSettings from "./scrollSettings"; @@ -40,7 +40,7 @@ export default function editorSettings() { prompt: strings["font size"], promptOptions: { required: true, - match: constants.FONT_SIZE, + match: config.FONT_SIZE, }, info: strings["settings-info-editor-font-size"], category: categories.textLayout, diff --git a/src/settings/helpSettings.js b/src/settings/helpSettings.js index bd8cdf35a..5088e902c 100644 --- a/src/settings/helpSettings.js +++ b/src/settings/helpSettings.js @@ -1,5 +1,5 @@ import settingsPage from "components/settingsPage"; -import constants from "lib/constants"; +import config from "lib/config"; export default function help() { const title = strings.help; @@ -7,25 +7,25 @@ export default function help() { { key: "docs", text: strings.documentation, - link: constants.DOCS_URL, + link: config.DOCS_URL, chevron: true, }, { key: "help", text: strings.help, - link: constants.TELEGRAM_URL, + link: config.TELEGRAM_URL, chevron: true, }, { key: "faqs", text: strings.faqs, - link: `${constants.WEBSITE_URL}/faqs`, + link: `${config.BASE_URL}/faqs`, chevron: true, }, { key: "bug_report", text: strings.bug_report, - link: `${constants.GITHUB_URL}/issues`, + link: `${config.GITHUB_URL}/issues`, chevron: true, }, ]; diff --git a/src/settings/mainSettings.js b/src/settings/mainSettings.js index dfaff6782..228770cc1 100644 --- a/src/settings/mainSettings.js +++ b/src/settings/mainSettings.js @@ -2,6 +2,7 @@ import settingsPage from "components/settingsPage"; import confirm from "dialogs/confirm"; import rateBox from "dialogs/rateBox"; import actionStack from "lib/actionStack"; +import config from "lib/config"; import openFile from "lib/openFile"; import removeAds from "lib/removeAds"; import appSettings from "lib/settings"; @@ -159,7 +160,7 @@ export default function mainSettings() { }, ]; - if (IS_FREE_VERSION) { + if (!config.HAS_PRO) { items.push({ key: "adRewards", text: strings["earn ad-free time"], diff --git a/src/settings/scrollSettings.js b/src/settings/scrollSettings.js index 0366f7280..0fcdaee8f 100644 --- a/src/settings/scrollSettings.js +++ b/src/settings/scrollSettings.js @@ -1,5 +1,5 @@ import settingsPage from "components/settingsPage"; -import constants from "lib/constants"; +import config from "lib/config"; import appSettings from "lib/settings"; export default function scrollSettings() { @@ -56,13 +56,13 @@ export default function scrollSettings() { function getScrollSpeedString(speed) { switch (speed) { - case constants.SCROLL_SPEED_FAST: + case config.SCROLL_SPEED_FAST: return strings.fast; - case constants.SCROLL_SPEED_SLOW: + case config.SCROLL_SPEED_SLOW: return strings.slow; - case constants.SCROLL_SPEED_FAST_X2: + case config.SCROLL_SPEED_FAST_X2: return `${strings.fast} x2`; - case constants.SCROLL_SPEED_NORMAL: + case config.SCROLL_SPEED_NORMAL: return strings.normal; default: return strings.normal; diff --git a/src/sidebarApps/extensions/index.js b/src/sidebarApps/extensions/index.js index 56584fb35..d3f7c5a3c 100644 --- a/src/sidebarApps/extensions/index.js +++ b/src/sidebarApps/extensions/index.js @@ -1,14 +1,12 @@ import "./style.scss"; - import fsOperation from "fileSystem"; -import ajax from "@deadlyjack/ajax"; import collapsableList from "components/collapsableList"; import Sidebar from "components/sidebar"; import alert from "dialogs/alert"; import prompt from "dialogs/prompt"; import select from "dialogs/select"; import purchaseListener from "handlers/purchase"; -import constants from "lib/constants"; +import config from "lib/config"; import InstallState from "lib/installState"; import loadPlugin from "lib/loadPlugin"; import settings from "lib/settings"; @@ -33,11 +31,10 @@ let isLoading = false; let currentFilter = null; let filterHasMore = true; let isFilterLoading = false; -const SUPPORTED_EDITOR = "cm"; function withSupportedEditor(url) { const separator = url.includes("?") ? "&" : "?"; - return `${url}${separator}supported_editor=${SUPPORTED_EDITOR}`; + return `${url}${separator}supported_editor=${config.SUPPORTED_EDITOR}`; } const $header = ( @@ -151,7 +148,7 @@ async function loadMorePlugins() { const response = await fetch( withSupportedEditor( - `${constants.API_BASE}/plugins?page=${currentPage}&limit=${LIMIT}`, + `${config.API_BASE}/plugins?page=${currentPage}&limit=${LIMIT}`, ), ); const newPlugins = await response.json(); @@ -236,9 +233,7 @@ async function searchPlugin() { try { $searchResult.classList.add("loading"); const plugins = await fsOperation( - withSupportedEditor( - Url.join(constants.API_BASE, `plugins?name=${query}`), - ), + withSupportedEditor(Url.join(config.API_BASE, `plugins?name=${query}`)), ).readFile("json"); installedPlugins = await listInstalledPlugins(); @@ -428,7 +423,7 @@ async function loadExplore() { const response = await fetch( withSupportedEditor( - `${constants.API_BASE}/plugins?page=${currentPage}&limit=${LIMIT}`, + `${config.API_BASE}/plugins?page=${currentPage}&limit=${LIMIT}`, ), ); const plugins = await response.json(); @@ -495,13 +490,13 @@ async function getFilteredPlugins(filterState) { if (filterState.value === "top_rated") { response = await fetch( withSupportedEditor( - `${constants.API_BASE}/plugins?explore=random&page=${page}&limit=${LIMIT}`, + `${config.API_BASE}/plugins?explore=random&page=${page}&limit=${LIMIT}`, ), ); } else { response = await fetch( withSupportedEditor( - `${constants.API_BASE}/plugin?orderBy=${filterState.value}&page=${page}&limit=${LIMIT}`, + `${config.API_BASE}/plugin?orderBy=${filterState.value}&page=${page}&limit=${LIMIT}`, ), ); } @@ -542,7 +537,7 @@ async function getFilteredPlugins(filterState) { const page = filterState.nextPage; const response = await fetch( withSupportedEditor( - `${constants.API_BASE}/plugins?page=${page}&limit=${LIMIT}`, + `${config.API_BASE}/plugins?page=${page}&limit=${LIMIT}`, ), ); const data = await response.json(); @@ -769,7 +764,7 @@ function ListItem({ icon, name, id, version, downloads, installed, source }) { try { let purchaseToken; let product; - const pluginUrl = Url.join(constants.API_BASE, `plugin/${id}`); + const pluginUrl = Url.join(config.API_BASE, `plugin/${id}`); const remotePlugin = await fsOperation(pluginUrl) .readFile("json") .catch(() => { @@ -803,12 +798,13 @@ function ListItem({ icon, name, id, version, downloads, installed, source }) { async function onpurchase(e) { const purchase = await getPurchase(product.productId); - await ajax.post(Url.join(constants.API_BASE, "plugin/order"), { - data: { + await fetch(Url.join(config.API_BASE, "plugin/order"), { + method: "POST", + body: JSON.stringify({ id: remotePlugin.id, token: purchase?.purchaseToken, package: BuildInfo.packageName, - }, + }), }); purchaseToken = purchase?.purchaseToken; } diff --git a/src/sidebarApps/sidebarApp.js b/src/sidebarApps/sidebarApp.js index 0c85339bc..49f43a984 100644 --- a/src/sidebarApps/sidebarApp.js +++ b/src/sidebarApps/sidebarApp.js @@ -3,7 +3,7 @@ let $apps; /**@type {HTMLElement} */ let $sidebar; /**@type {HTMLElement} */ -let $contaienr; +let $container; export default class SidebarApp { /**@type {HTMLSpanElement} */ @@ -96,14 +96,14 @@ export default class SidebarApp { // Try to replace the old container, or append if it's not in the DOM try { if (oldContainer && oldContainer.parentNode === $sidebar) { - $sidebar.replaceChild($contaienr, oldContainer); + $sidebar.replaceChild($container, oldContainer); } else { // Old container not in sidebar, just append the new one const existingContainer = $sidebar.get(".container"); if (existingContainer) { - $sidebar.replaceChild($contaienr, existingContainer); + $sidebar.replaceChild($container, existingContainer); } else { - $sidebar.appendChild($contaienr); + $sidebar.appendChild($container); } } } catch (error) { @@ -113,7 +113,7 @@ export default class SidebarApp { if (existingContainer) { existingContainer.remove(); } - $sidebar.appendChild($contaienr); + $sidebar.appendChild($container); } this.#onselect(this.#container); } @@ -171,10 +171,10 @@ function Icon({ icon, id, title }) { * @returns {HTMLElement} */ function getContainer($el) { - const res = $contaienr; + const res = $container; if ($el) { - $contaienr = $el; + $container = $el; } return res || $sidebar.get(".container"); diff --git a/src/utils/helpers.js b/src/utils/helpers.js index d9337ef4d..5ac96100e 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -1,10 +1,9 @@ import fsOperation from "fileSystem"; -import ajax from "@deadlyjack/ajax"; import { getModeForPath as getCMModeForPath } from "cm/modelist"; import alert from "dialogs/alert"; import escapeStringRegexp from "escape-string-regexp"; import adRewards from "lib/adRewards"; -import constants from "lib/constants"; +import config from "lib/config"; import path from "./Path"; import Uri from "./Uri"; import Url from "./Url"; @@ -289,7 +288,7 @@ export default { editorManager.emit("update", "file-delete"); }, canShowAds() { - return Boolean(IS_FREE_VERSION && adRewards.canShowAds()); + return Boolean(!config.HAS_PRO && adRewards.canShowAds()); }, async showInterstitialIfReady() { if (!this.canShowAds()) return false; @@ -330,8 +329,8 @@ export default { }, async checkAPIStatus() { try { - const { status } = await ajax.get(Url.join(constants.API_BASE, "status")); - return status === "ok"; + const res = await fetch(Url.join(config.API_BASE, "status")); + return res.ok; } catch (error) { window.log("error", error); return false; diff --git a/utils/lang.js b/utils/lang.js index d3390e426..78b0dc1a8 100755 --- a/utils/lang.js +++ b/utils/lang.js @@ -83,7 +83,7 @@ function addToAllFiles() { } strings[key] = val; - const newText = JSON.stringify(strings, undefined, 2); + const newText = JSON.stringify(strings, undefined, "\t"); fs.writeFileSync(file, newText, "utf8"); console.log(`${lang}: Added ✓`); addedCount++; @@ -161,7 +161,7 @@ function bulkAddStrings() { } if (addedCount > 0) { - const newText = JSON.stringify(strings, undefined, 2); + const newText = JSON.stringify(strings, undefined, "\t"); fs.writeFileSync(file, newText, "utf8"); } @@ -257,7 +257,7 @@ async function update() { if (flagError) { if (fix) { total += 1; - const langJSONData = JSON.stringify(langData, undefined, 2); + const langJSONData = JSON.stringify(langData, undefined, "\t"); fs.writeFile(langFile, langJSONData, (err) => { if (err) { console.error(err); @@ -353,7 +353,7 @@ async function update() { const text = fs.readFileSync(file, "utf8"); const strings = modify(JSON.parse(text)); if (strings) { - const newText = JSON.stringify(strings, undefined, 2); + const newText = JSON.stringify(strings, undefined, "\t"); fs.writeFile(file, newText, "utf8", (err) => { if (err) { console.error(err);