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..26273cc14 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "cordova-plugin-server": {}, "cordova-plugin-ftp": {}, "cordova-plugin-sdcard": {}, - "cordova-plugin-iap": {}, "cordova-plugin-advanced-http": { "ANDROIDBLACKLISTSECURESOCKETPROTOCOLS": "SSLv3,TLSv1" }, @@ -37,12 +36,13 @@ "cordova-plugin-buildinfo": {}, "cordova-plugin-browser": {}, "cordova-plugin-sftp": {}, - "com.foxdebug.acode.rk.exec.proot": {}, "com.foxdebug.acode.rk.exec.terminal": {}, "com.foxdebug.acode.rk.customtabs": {}, "com.foxdebug.acode.rk.plugin.plugincontext": {}, + "cordova-plugin-system": {}, "com.foxdebug.acode.rk.auth": {}, - "cordova-plugin-system": {} + "cordova-plugin-iap": {}, + "com.foxdebug.acode.rk.exec.proot": {} }, "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 d20ab446e..4fc5504ee 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": "Pembesaran UI", - "settings-info-app-ui-zoom": "Skalakan teks di seluruh antarmuka Acode." + "settings-info-app-ui-zoom": "Skalakan teks di seluruh antarmuka Acode.", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." } 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 c4beb297d..80013d555 100644 --- a/src/lang/zh-cn.json +++ b/src/lang/zh-cn.json @@ -726,5 +726,7 @@ "auto close tags": "自动补全闭合标签", "settings-info-editor-auto-close-tags": "在 HTML、XML、Vue、Angular 和 PHP 模板文件中自动插入闭合标签。", "ui zoom": "UI 缩放", - "settings-info-app-ui-zoom": "缩放所有 Acode 界面文字。" + "settings-info-app-ui-zoom": "缩放所有 Acode 界面文字。", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." } diff --git a/src/lang/zh-hant.json b/src/lang/zh-hant.json index b94ace6d9..2ba300a0b 100644 --- a/src/lang/zh-hant.json +++ b/src/lang/zh-hant.json @@ -726,5 +726,7 @@ "auto close tags": "自動補全閉合標籤", "settings-info-editor-auto-close-tags": "在 HTML、XML、Vue、Angular 和 PHP 模板文件中自動插入閉合標籤。", "ui zoom": "UI 縮放", - "settings-info-app-ui-zoom": "縮放所有 Acode 介面文字。" + "settings-info-app-ui-zoom": "縮放所有 Acode 介面文字。", + "plugin-not-supported": "Plugin not supported", + "plugin-not-supported-info": "The plugin was created for older version of Acode." } 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..350ea2fd0 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 = {}; @@ -322,6 +321,7 @@ export default class Acode { view: cmView, }); + this.define("config", config); this.define("Url", Url); this.define("page", Page); this.define("Color", Color); @@ -522,10 +522,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 +578,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 64% rename from src/lib/constants.js rename to src/lib/config.js index f1315999a..0fa05e784 100644 --- a/src/lib/constants.js +++ b/src/lib/config.js @@ -1,4 +1,9 @@ -export default { +const BASE_URL = "https://acode.app"; +let hasPro = false; + +const config = { + 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", @@ -16,21 +21,47 @@ export default { CUSTOM_THEME: 'body[theme="custom"]', FEEDBACK_EMAIL: "acode@foxdebug.com", ERUDA_CDN: "https://cdn.jsdelivr.net/npm/eruda", + 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", + + //assume playstore build until proven otherwise + IAP_AVAILABLE: true, + + get HAS_PRO() { + return hasPro; + }, + + set HAS_PRO(value) { + hasPro = value; + }, }; + + +cordova.exec((installer) => { + config.IAP_AVAILABLE = + typeof iap !== "undefined" && + installer != null && + installer !== "null" && + installer === "com.android.vending"; + }, + (error) => { + console.error(error); + config.IAP_AVAILABLE = typeof iap !== "undefined"; + }, 'System', 'getInstaller', []); + +export default config; 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..eb6112184 100644 --- a/src/pages/plugin/plugin.js +++ b/src/pages/plugin/plugin.js @@ -1,12 +1,12 @@ 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 auth from "lib/auth"; +import config from "lib/config"; import installPlugin from "lib/installPlugin"; import InstallState from "lib/installState"; import settings from "lib/settings"; @@ -55,6 +55,7 @@ export default async function PluginInclude( let purchaseToken; let $settingsIcon; let minVersionCode = -1; + let isSupported = true; actionStack.push({ id: "plugin", @@ -138,9 +139,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 +163,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 +267,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 +299,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 +378,7 @@ export default async function PluginInclude( uninstall, currentVersion, minVersionCode, + isSupported, }); // Handle anchor links @@ -495,7 +510,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..c0eb3a35e 100644 --- a/src/pages/plugin/plugin.view.js +++ b/src/pages/plugin/plugin.view.js @@ -9,7 +9,8 @@ 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 auth from "lib/auth"; +import config from "lib/config"; import helpers from "utils/helpers"; import Url from "utils/Url"; @@ -53,6 +54,7 @@ export default (props) => { license, changelogs, repository, + isSupported = true, keywords: keywordsRaw, contributors: contributorsRaw, votes_up: votesUp, @@ -209,7 +211,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}`; } @@ -270,6 +272,7 @@ function handleTabClick(e) { } function Buttons({ + id, name, isPaid, installed, @@ -280,7 +283,46 @@ function Buttons({ price, buy, minVersionCode, + isSupported = true, }) { + async function openPluginWebsite() { + try { + const user = await auth.getLoggedInUser(); + if (!user) { + CustomTabs.open( + `${config.BASE_URL}/login?redirect=app`, + { showTitle: true }, + () => {}, + () => {}, + ); + return; + } + + CustomTabs.open( + `${config.BASE_URL}/plugin/${id}`, + { showTitle: true }, + () => {}, + () => {}, + ); + } catch (e) { + console.error(e); + } + } + if (!isSupported) { + return ( +
+

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

+ {strings["plugin-not-supported-info"]} +
+ ); + } + if ( typeof minVersionCode === "number" && minVersionCode > BuildInfo.versionCode @@ -288,7 +330,7 @@ function Buttons({ return (
- + {strings["plugin min version"] .replace("{name}", name) .replace("{v-code}", minVersionCode)} @@ -329,6 +371,19 @@ function Buttons({ ); } + if (!config.IAP_AVAILABLE && isPaid && !purchased && price) { + return ( + + ); + } + if (isPaid && !purchased && price) { return (